//! # [`BuildRequest`] - the core of the build process //! //! The [`BuildRequest`] object is the core of the build process. It contains all the resolved arguments //! flowing in from the CLI, dioxus.toml, env vars, and the workspace. //! //! Every BuildRequest is tied to a given workspace and BuildArgs. For simplicity's sake, the BuildArgs //! struct is used to represent the CLI arguments and all other configuration is basically just //! extra CLI arguments, but in a configuration format. //! //! When [`BuildRequest::build`] is called, it will prepare its work directory in the target folder //! and then start running the build process. A [`BuildContext`] is required to customize this //! build process, containing a channel for progress updates and the build mode. //! //! The [`BuildMode`] is extremely important since it influences how the build is performed. Most //! "normal" builds just use [`BuildMode::Base`], but we also support [`BuildMode::Fat`] and //! [`BuildMode::Thin`]. These builds are used together to power the hot-patching and fast-linking //! engine. //! - BuildMode::Base: A normal build generated using `cargo rustc` //! - BuildMode::Fat: A "fat" build where all dependency rlibs are merged into a static library //! - BuildMode::Thin: A "thin" build that dynamically links against the artifacts produced by the "fat" build //! //! The BuildRequest is also responsible for writing the final build artifacts to disk. This includes //! //! - Writing the executable //! - Processing assets from the artifact //! - Writing any metadata or configuration files (Info.plist, AndroidManifest.xml) //! - Bundle splitting (for wasm) and wasm-bindgen //! //! In some cases, the BuildRequest also handles the linking of the final executable. Specifically, //! - For Android, we use `dx` as an opaque linker to dynamically find the true android linker //! - For hotpatching, the CLI manually links the final executable with a stub file //! //! ## Build formats: //! //! We support building for the most popular platforms: //! - Web via wasm-bindgen //! - macOS via app-bundle //! - iOS via app-bundle //! - Android via gradle //! - Linux via app-image //! - Windows via exe, msi/msix //! //! Note that we are missing some setups that we *should* support: //! - PWAs, WebWorkers, ServiceWorkers //! - Web Extensions //! - Linux via flatpak/snap //! //! There are some less popular formats that we might want to support eventually: //! - TVOS, watchOS //! - OpenHarmony //! //! Also, some deploy platforms have their own bespoke formats: //! - Cloudflare workers //! - AWS Lambda //! //! Currently, we defer most of our deploy-based bundling to Tauri bundle, though we should migrate //! to just bundling everything ourselves. This would require us to implement code-signing which //! is a bit of a pain, but fortunately a solved process (). //! //! ## Build Structure //! //! Builds generally follow the same structure everywhere: //! - A main executable //! - Sidecars (alternate entrypoints, framewrok plugins, etc) //! - Assets (images, fonts, etc) //! - Metadata (Info.plist, AndroidManifest.xml) //! - Glue code (java, kotlin, javascript etc) //! - Entitlements for code-signing and verification //! //! We need to be careful to not try and put a "round peg in a square hole," but most platforms follow //! the same pattern. //! //! As such, we try to assemble a build directory that's somewhat sensible: //! - A main "staging" dir for a given app //! - Per-profile dirs (debug/release) //! - A platform dir (ie web/desktop/android/ios) //! - The "bundle" dir which is basically the `.app` format or `wwww` dir. //! - The "executable" dir where the main exe is housed //! - The "assets" dir where the assets are housed //! - The "meta" dir where stuff like Info.plist, AndroidManifest.xml, etc are housed //! //! There's also some "quirky" folders that need to be stable between builds but don't influence the //! bundle itself: //! - session_cache_dir which stores stuff like window position //! //! ### Windows / Linux: //! //! Windows does not provide an AppImage format, so instead we're going build the same folder //! structure as an AppImage, but when distributing, we'll create a .exe that embeds the resources //! as an embedded .zip file. When the app runs, it will implicitly unzip its resources into the //! Program Files folder. Any subsequent launches of the parent .exe will simply call the AppRun.exe //! entrypoint in the associated Program Files folder. //! //! This is, in essence, the same as an installer, so we might eventually just support something like msi/msix //! which functionally do the same thing but with a sleeker UI. //! //! This means no installers are required and we can bake an updater into the host exe. //! //! ## Handling asset lookups: //! current_exe.join("assets") //! ``` //! app.appimage/ //! main.exe //! main.desktop //! package.json //! assets/ //! logo.png //! ``` //! //! Since we support just a few locations, we could just search for the first that exists //! - usr //! - ../Resources //! - assets //! - Assets //! - $cwd/assets //! //! ``` //! assets::root() -> //! mac -> ../Resources/ //! ios -> ../Resources/ //! android -> assets/ //! server -> assets/ //! liveview -> assets/ //! web -> /assets/ //! root().join(bundled) //! ``` //! //! Every dioxus app can have an optional server executable which will influence the final bundle. //! This is built in parallel with the app executable during the `build` phase and the progres/status //! of the build is aggregated. //! //! The server will *always* be dropped into the `web` folder since it is considered "web" in nature, //! and will likely need to be combined with the public dir to be useful. //! //! We do our best to assemble read-to-go bundles here, such that the "bundle" step for each platform //! can just use the build dir //! //! When we write the AppBundle to a folder, it'll contain each bundle for each platform under the app's name: //! ``` //! dog-app/ //! build/ //! web/ //! server.exe //! assets/ //! some-secret-asset.txt (a server-side asset) //! public/ //! index.html //! assets/ //! logo.png //! desktop/ //! App.app //! App.appimage //! App.exe //! server/ //! server //! assets/ //! some-secret-asset.txt (a server-side asset) //! ios/ //! App.app //! App.ipa //! android/ //! App.apk //! bundle/ //! build.json //! Desktop.app //! Mobile_x64.ipa //! Mobile_arm64.ipa //! Mobile_rosetta.ipa //! web.appimage //! web/ //! server.exe //! assets/ //! some-secret-asset.txt //! public/ //! index.html //! assets/ //! logo.png //! style.css //! ``` //! //! When deploying, the build.json file will provide all the metadata that dx-deploy will use to //! push the app to stores, set up infra, manage versions, etc. //! //! The format of each build will follow the name plus some metadata such that when distributing you //! can easily trim off the metadata. //! //! The idea here is that we can run any of the programs in the same way that they're deployed. //! //! ## Bundle structure links //! - apple: //! - appimage: //! //! ## Extra links //! - xbuild: use super::HotpatchModuleCache; use crate::{ opt::{process_file_to, AppManifest}, WorkspaceRustcArgs, }; use crate::{ AndroidTools, BuildContext, BuildId, BundleFormat, DioxusConfig, LinkAction, Platform, Renderer, Result, RustcArgs, TargetArgs, Workspace, DX_RUSTC_WRAPPER_ENV_VAR, }; use anyhow::{bail, Context}; use cargo_metadata::diagnostic::Diagnostic; use cargo_toml::{Profile, Profiles, StripSetting}; use depinfo::RustcDepInfo; use dioxus_cli_config::PRODUCT_NAME_ENV; use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV}; use krates::{cm::TargetKind, NodeId}; use manganis::BundledAsset; use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; use serde::Deserialize; use std::{borrow::Cow, collections::VecDeque, ffi::OsString}; use std::{ collections::HashSet, path::{Path, PathBuf}, process::Stdio, sync::{ atomic::{AtomicUsize, Ordering}, Arc, }, time::SystemTime, }; use target_lexicon::{Architecture, OperatingSystem, Triple}; use tempfile::TempDir; use tokio::{io::AsyncBufReadExt, process::Command}; /// This struct is used to plan the build process. /// /// The point here is to be able to take in the user's config from the CLI without modifying the /// arguments in place. Creating a buildplan "resolves" their config into a build plan that can be /// introspected. For example, the users might not specify a "Triple" in the CLI but the triple will /// be guaranteed to be resolved here. /// /// Creating a buildplan also lets us introspect build requests and modularize our build process. /// This will, however, lead to duplicate fields between the CLI and the build engine. This is fine /// since we have the freedom to evolve the schema internally without breaking the API. /// /// All updates from the build will be sent on a global "BuildProgress" channel. #[derive(Clone)] pub(crate) struct BuildRequest { pub(crate) workspace: Arc, pub(crate) config: DioxusConfig, pub(crate) crate_package: NodeId, pub(crate) crate_target: krates::cm::Target, pub(crate) profile: String, pub(crate) release: bool, pub(crate) bundle: BundleFormat, pub(crate) triple: Triple, pub(crate) device_name: Option, pub(crate) should_codesign: bool, pub(crate) package: String, pub(crate) main_target: String, pub(crate) features: Vec, pub(crate) rustflags: cargo_config2::Flags, pub(crate) extra_cargo_args: Vec, pub(crate) extra_rustc_args: Vec, pub(crate) all_features: bool, pub(crate) target_dir: PathBuf, pub(crate) skip_assets: bool, pub(crate) wasm_split: bool, pub(crate) debug_symbols: bool, pub(crate) keep_names: bool, pub(crate) inject_loading_scripts: bool, pub(crate) custom_linker: Option, pub(crate) base_path: Option, pub(crate) using_dioxus_explicitly: bool, pub(crate) apple_entitlements: Option, pub(crate) apple_team_id: Option, pub(crate) version_code: Option, pub(crate) bundle_version: Option, pub(crate) session_cache_dir: PathBuf, pub(crate) raw_json_diagnostics: bool, pub(crate) windows_subsystem: Option, } /// dx can produce different "modes" of a build. A "regular" build is a "base" build. The Fat and Thin /// modes are used together to achieve binary patching and linking. /// /// Guide: /// ---------- /// - Base: A normal build generated using `cargo rustc`, intended for production use cases /// /// - Fat: A "fat" build with -Wl,-all_load and no_dead_strip, keeping *every* symbol in the binary. /// Intended for development for larger up-front builds with faster link times and the ability /// to binary patch the final binary. On WASM, this also forces wasm-bindgen to generate all /// JS-WASM bindings, saving us the need to re-wasmbindgen the final binary. /// /// - Thin: A "thin" build that dynamically links against the dependencies produced by the "fat" build. /// This is generated by calling rustc *directly* and might be more fragile to construct, but /// generates *much* faster than a regular base or fat build. #[allow(clippy::large_enum_variant)] #[derive(Clone, Debug, PartialEq)] pub enum BuildMode { /// A normal build generated using `cargo rustc` /// /// "run" indicates whether this build is intended to be run immediately after building. /// This means we try to capture the build environment, saving vars like `CARGO_MANIFEST_DIR` /// for the running executable. Base { run: bool }, /// A "Fat" build generated with cargo rustc and dx as a custom linker without -Wl,-dead-strip Fat, /// A "thin" build generated with `rustc` directly and dx as a custom linker Thin { /// List of changed files causing this rebuild. Mostly used for diagnostics changed_files: Vec, /// The ASLR slide of the running program, used to hardcode symbol jumps aslr_reference: u64, /// The captured RustcArgs for every crate in the workspace, collected by RUSTC_WORKSPACE_WRAPPER /// This is used for replaying rustc invocations for workspace hotpatching workspace_rustc_args: WorkspaceRustcArgs, /// Cumulative set of all workspace crates modified since the fat build. modified_crates: HashSet, /// Cache of initial binary parsing which speeds up stub creation cache: Arc, }, } /// The end result of a build. /// /// Contains the final asset manifest, the executable, and metadata about the build. /// Note that the `exe` might be stale and/or overwritten by the time you read it! /// /// The patch cache is only populated on fat builds and then used for thin builds (see `BuildMode::Thin`). #[derive(Clone, Debug)] pub struct BuildArtifacts { pub(crate) root_dir: PathBuf, pub(crate) exe: PathBuf, pub(crate) workspace_rustc: WorkspaceRustcArgs, pub(crate) time_start: SystemTime, pub(crate) time_end: SystemTime, pub(crate) assets: AppManifest, pub(crate) mode: BuildMode, pub(crate) patch_cache: Option>, pub(crate) depinfo: RustcDepInfo, pub(crate) build_id: BuildId, } impl BuildRequest { /// Create a new build request. /// /// This method consolidates various inputs into a single source of truth. It combines: /// - Command-line arguments provided by the user. /// - The crate's `Cargo.toml`. /// - The `dioxus.toml` configuration file. /// - User-specific CLI settings. /// - The workspace metadata. /// - Host-specific details (e.g., Android tools, installed frameworks). /// - The intended target platform. /// /// Fields may be duplicated from the inputs to allow for autodetection and resolution. /// /// Autodetection is performed for unspecified fields where possible. /// /// Note: Build requests are typically created only when the CLI is invoked or when significant /// changes are detected in the `Cargo.toml` (e.g., features added or removed). pub(crate) async fn new(args: &TargetArgs, workspace: Arc) -> Result { let crate_package = workspace.find_main_package(args.package.clone())?; let target_kind = match args.example.is_some() { true => TargetKind::Example, false => TargetKind::Bin, }; let main_package = &workspace.krates[crate_package]; let target_name = args .example .clone() .or(args.bin.clone()) .or_else(|| { if let Some(default_run) = &main_package.default_run { return Some(default_run.to_string()); } let bin_count = main_package .targets .iter() .filter(|x| x.kind.contains(&target_kind)) .count(); if bin_count != 1 { return None; } main_package.targets.iter().find_map(|x| { if x.kind.contains(&target_kind) { Some(x.name.clone()) } else { None } }) }) .unwrap_or(workspace.krates[crate_package].name.clone()); // Use the main_target for the client + server build if it is set, otherwise use the target name for this // specific build. This is important for @client @server syntax so we use the client's output directory for the bundle. let main_target = args.client_target.clone().unwrap_or(target_name.clone()); let crate_target = main_package .targets .iter() .find(|target| { target_name == target.name.as_str() && target.kind.contains(&target_kind) }) .with_context(|| { let target_of_kind = |kind|-> String { let filtered_packages = main_package .targets .iter() .filter_map(|target| { target.kind.contains(kind).then_some(target.name.as_str()) }).collect::>(); filtered_packages.join(", ")}; if let Some(example) = &args.example { let examples = target_of_kind(&TargetKind::Example); format!("Failed to find example {example}. \nAvailable examples are:\n{examples}") } else if let Some(bin) = &args.bin { let binaries = target_of_kind(&TargetKind::Bin); format!("Failed to find binary {bin}. \nAvailable binaries are:\n{binaries}") } else { format!("Failed to find target {target_name}. \nIt looks like you are trying to build dioxus in a library crate. \ You either need to run dx from inside a binary crate or build a specific example with the `--example` flag. \ Available examples are:\n{}", target_of_kind(&TargetKind::Example)) } })? .clone(); // Load config from Dioxus.toml and/or inline config in the target's source file. // Inline config in doc comments takes precedence over Dioxus.toml. let config = workspace .load_dioxus_config(crate_package, Some(crate_target.src_path.as_std_path()))? .unwrap_or_default(); // 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(); 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 --` 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: Fullstack usage is inferred from the presence of the fullstack feature or --fullstack. */ let mut features = args.features.clone(); let no_default_features = args.no_default_features; let all_features = args.all_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) }; // 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(|| { // If multiple renderers are enabled, pick the first non-server one if enabled_renderers.len() == 2 && enabled_renderers .iter() .any(|f| matches!(f.0, Renderer::Server)) { return Some( enabled_renderers .iter() .find(|f| !matches!(f.0, Renderer::Server)) .cloned() .unwrap(), ); } None }) .or_else(|| { // Pick the first non-server feature in the cargo.toml let non_server_features = known_features_as_renderers .iter() .filter(|f| f.1.as_str() != "server") .collect::>(); if non_server_features.len() == 1 { Some(non_server_features[0].clone()) } else { None } }); 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)); } } // 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()?)); } 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())); } 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())); } 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())); } 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)); 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)); // maybe probe adb? if let Some(_device_name) = device.as_ref() { if triple.is_none() { triple = Some( AndroidTools::current() .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())); } 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())); } } // If default features are enabled, we need to add the default features // which don't enable a renderer 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 if let Some(renderer) = renderer { if let Some(feature) = Self::feature_for_platform_and_renderer(main_package, &triple, renderer) { features.push(feature); features.dedup(); } } // Set the profile of the build if it's not already set // This is mostly used for isolation of builds (preventing thrashing) but also useful to have multiple performance profiles // We might want to move some of these profiles into dioxus.toml and make them "virtual". let profile = match args.profile.clone() { Some(profile) => profile, None => bundle.profile_name(args.release), }; // Determine if we should codesign let should_codesign = args.codesign || device.is_some() || args.apple_entitlements.is_some(); // Determining release mode is based on the profile, actually, so we need to check that let release = workspace.is_release_profile(&profile); // Determine the --package we'll pass to cargo. // todo: I think this might be wrong - we don't want to use main_package necessarily... let package = args .package .clone() .unwrap_or_else(|| main_package.name.clone()); // Somethings we override are also present in the user's config. // If we can't get them by introspecting cargo, then we need to get them from the config // // This involves specifically two fields: // - The linker since we override it for Android and hotpatching // - RUSTFLAGS since we also override it for Android and hotpatching let cargo_config = cargo_config2::Config::load().unwrap(); let mut custom_linker = cargo_config.linker(triple.to_string()).ok().flatten(); let mut rustflags = cargo_config2::Flags::default(); // Make sure to take into account the RUSTFLAGS env var and the CARGO_TARGET__RUSTFLAGS for env in [ "RUSTFLAGS".to_string(), format!("CARGO_TARGET_{triple}_RUSTFLAGS"), ] { if let Ok(flags) = std::env::var(env) { rustflags .flags .extend(cargo_config2::Flags::from_space_separated(&flags).flags); } } // Use the user's linker if the specify it at the target level if let Ok(target) = cargo_config.target(triple.to_string()) { if let Some(flags) = target.rustflags { rustflags.flags.extend(flags.flags); } } // When we do android builds we need to make sure we link against the android libraries // We also `--export-dynamic` to make sure we can do shenanigans like `dlsym` the `main` symbol if matches!(bundle, BundleFormat::Android) { rustflags.flags.extend([ "-Clink-arg=-landroid".to_string(), "-Clink-arg=-llog".to_string(), "-Clink-arg=-lOpenSLES".to_string(), "-Clink-arg=-lc++abi".to_string(), "-Clink-arg=-Wl,--export-dynamic".to_string(), format!( "-Clink-arg=-Wl,--sysroot={}", workspace.android_tools()?.sysroot().display() ), ]); } // Make sure we set the sysroot for ios builds in the event the user doesn't have it set if matches!(bundle, BundleFormat::Ios) && matches!( triple.operating_system, target_lexicon::OperatingSystem::IOS(_) ) { let xcode_path = Workspace::get_xcode_path() .await .unwrap_or_else(|| "/Applications/Xcode.app".to_string().into()); let sysroot_location = match triple.environment { target_lexicon::Environment::Sim => xcode_path .join("Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk"), _ => { // If the target has been determined as the iOS x86 simulator above if triple.to_string() == "x86_64-apple-ios" { xcode_path.join( "Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk", ) } else { xcode_path.join("Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk") } } }; if sysroot_location.exists() && !rustflags.flags.iter().any(|f| f == "-isysroot") { rustflags.flags.extend([ "-Clink-arg=-isysroot".to_string(), format!("-Clink-arg={}", sysroot_location.display()), ]); } } // 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, ); } // If no custom linker is set, then android falls back to us as the linker if custom_linker.is_none() && bundle == BundleFormat::Android { let min_sdk_version = config.application.android_min_sdk_version.unwrap_or(28); custom_linker = Some( workspace .android_tools()? .android_cc(&triple, min_sdk_version), ); } let target_dir = std::env::var("CARGO_TARGET_DIR") .ok() .map(PathBuf::from) .or_else(|| cargo_config.build.target_dir.clone()) .unwrap_or_else(|| workspace.workspace_root().join("target")); // If the user provided a profile and wasm_split is enabled, we should check that LTO=true and debug=true if args.wasm_split { if let Some(profile_data) = workspace.cargo_toml.profile.custom.get(&profile) { use cargo_toml::{DebugSetting, LtoSetting}; if matches!(profile_data.lto, Some(LtoSetting::None) | None) { tracing::warn!("wasm-split requires LTO to be enabled in the profile. \ Please set `lto = true` in the `[profile.{profile}]` section of your Cargo.toml"); } if matches!(profile_data.debug, Some(DebugSetting::None) | None) { tracing::warn!("wasm-split requires debug symbols to be enabled in the profile. \ Please set `debug = true` in the `[profile.{profile}]` section of your Cargo.toml"); } } } #[allow(deprecated)] let session_cache_dir = args .session_cache_dir .clone() .unwrap_or_else(|| TempDir::new().unwrap().into_path()); let extra_rustc_args = shell_words::split(&args.rustc_args.clone().unwrap_or_default()) .context("Failed to parse rustc args")?; let extra_cargo_args = shell_words::split(&args.cargo_args.clone().unwrap_or_default()) .context("Failed to parse cargo args")?; tracing::debug!( r#"Target Info: • features: {features:?} • triple: {triple} • bundle format: {bundle:?} • session cache dir: {session_cache_dir:?} • linker: {custom_linker:?} • target_dir: {target_dir:?}"#, ); Ok(Self { features, bundle, all_features, crate_package, crate_target, profile, triple, device_name: device, workspace, config, target_dir, custom_linker, extra_rustc_args, extra_cargo_args, release, package, main_target, rustflags, using_dioxus_explicitly, should_codesign, session_cache_dir, skip_assets: args.skip_assets, base_path: args.base_path.clone(), wasm_split: args.wasm_split, debug_symbols: args.debug_symbols, keep_names: args.keep_names, inject_loading_scripts: args.inject_loading_scripts, apple_entitlements: args.apple_entitlements.clone(), apple_team_id: args.apple_team_id.clone(), version_code: args.version_code, bundle_version: args.bundle_version.clone(), raw_json_diagnostics: args.raw_json_diagnostics, windows_subsystem: args.windows_subsystem.clone(), }) } pub(crate) async fn prebuild(&self, ctx: &BuildContext) -> Result<()> { ctx.profile_phase("Prebuild"); // Create the session cache directory let cache_dir = self.session_cache_dir(); _ = std::fs::create_dir_all(&cache_dir); _ = std::fs::create_dir_all(self.rustc_wrapper_args_dir()); _ = std::fs::create_dir_all(self.rustc_wrapper_args_scope_dir(&ctx.mode)?); _ = std::fs::File::create(self.link_err_file()); _ = std::fs::File::create(self.link_args_file()); _ = std::fs::File::create(self.windows_command_file()); if !matches!(ctx.mode, BuildMode::Thin { .. }) { self.prepare_build_dir(ctx)?; } if !ctx.is_primary_build() { return Ok(()); } // Run the tailwind build before bundling anything else _ = crate::TailwindCli::run_once( self.package_manifest_dir(), self.config.application.tailwind_input.clone(), self.config.application.tailwind_output.clone(), ) .await; // We want to copy over the prebuilt OpenSSL binaries to ~/.dx/prebuilt/openssl- if self.bundle == BundleFormat::Android { AndroidTools::unpack_prebuilt_openssl()?; } // Compile the Windows resource (.res) so it's ready to be linked in later if matches!(self.triple.operating_system, OperatingSystem::Windows) { if let Err(err) = self.write_winres() { if self.using_dioxus_explicitly { tracing::warn!("Application may not have an icon: {err}"); } } } Ok(()) } pub(crate) async fn build(&self, ctx: BuildContext) -> Result { match &ctx.mode { // In hotpatch mode, we use the dedicated hotpatch flow BuildMode::Thin { .. } => self.compile_workspace_hotpatch(&ctx).await, // In base/fat mode, we do the full chain with a root `cargo rustc` BuildMode::Base { .. } | BuildMode::Fat => { let mut artifacts = self.cargo_build(&ctx).await?; ctx.profile_phase("Post-processing executable"); self.post_process_executable(&artifacts).await?; ctx.profile_phase("Writing executable"); self.write_executable(&ctx, &mut artifacts) .await .context("Failed to write executable")?; ctx.profile_phase("Writing frameworks"); self.write_frameworks(&artifacts) .await .context("Failed to write frameworks")?; ctx.profile_phase("Writing assets"); self.write_assets(&ctx, &artifacts.assets) .await .context("Failed to write assets")?; ctx.profile_phase("Writing metadata"); self.write_metadata() .await .context("Failed to write metadata")?; ctx.profile_phase("Writing ffi"); self.write_ffi_plugins(&ctx, &artifacts).await?; ctx.profile_phase("Running optimizer"); self.optimize(&ctx) .await .context("Failed to optimize build")?; ctx.profile_phase("Running assemble"); self.assemble(&ctx) .await .context("Failed to assemble build")?; ctx.profile_phase("Populating cache"); self.fill_caches(&ctx, &mut artifacts).await?; tracing::debug!("Bundle created at {}", self.root_dir().display()); Ok(artifacts) } } } /// Run the cargo build by assembling the build command and executing it. /// /// This method needs to be very careful with processing output since errors being swallowed will /// be very confusing to the user. /// /// This method is only meant to be run by fat/full builds - not by hotpatch builds pub async fn cargo_build(&self, ctx: &BuildContext) -> Result { let time_start = SystemTime::now(); // If we forget to do this, then we won't get the linker args since rust skips the full build // We need to make sure to not react to this though, so the filemap must cache it _ = self.bust_fingerprint(ctx); // Extract the unit count of the crate graph so build_cargo has more accurate data // "Thin" builds only build the final exe, so we only need to build one crate let crate_count = match ctx.mode { BuildMode::Thin { .. } => 1, _ => self.get_unit_count_estimate(&ctx.mode).await, }; // Spawn the `cargo rustc` or `rustc` command let mut child = self .cargo_build_command(&ctx.mode)? .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .context("Failed to spawn cargo build")?; // Direct rustc thin builds don't emit Cargo-style unit progress messages, so if we don't // advance the profiler here the entire compile winds up attributed to "Starting Build". ctx.status_starting_build(crate_count); let stdout = tokio::io::BufReader::new(child.stdout.take().unwrap()); let stderr = tokio::io::BufReader::new(child.stderr.take().unwrap()); let mut output_location: Option = None; let mut stdout = stdout.lines(); let mut stderr = stderr.lines(); let mut units_compiled = 0; let mut emitting_error = false; loop { use cargo_metadata::Message; let line = tokio::select! { Ok(Some(line)) = stdout.next_line() => line, Ok(Some(line)) = stderr.next_line() => line, else => break, }; // If raw JSON diagnostics are requested, relay the line directly if self.raw_json_diagnostics { println!("{}", line); } let Some(Ok(message)) = Message::parse_stream(std::io::Cursor::new(line)).next() else { continue; }; match message { Message::BuildScriptExecuted(_) => units_compiled += 1, Message::CompilerMessage(msg) => ctx.status_build_diagnostic(msg.message), Message::TextLine(line) => { // Handle the case where we're getting lines directly from rustc. // These are in a different format than the normal cargo output, though I imagine // this parsing code is quite fragile/sensitive to changes in cargo, cargo_metadata, rustc, etc. #[derive(Deserialize)] struct RustcArtifact { artifact: PathBuf, emit: String, } // These outputs look something like: // // { "artifact":"target/debug/deps/libdioxus_core-4f2a0b3c1e5f8b7c.rlib", "emit":"link" } // // There are other outputs like depinfo that we might be interested in the future. if let Ok(artifact) = serde_json::from_str::(&line) { if artifact.emit == "link" { output_location = Some(artifact.artifact); } } // Handle direct rustc diagnostics if let Ok(diag) = serde_json::from_str::(&line) { ctx.status_build_diagnostic(diag); } // For whatever reason, if there's an error while building, we still receive the TextLine // instead of an "error" message. However, the following messages *also* tend to // be the error message, and don't start with "error:". So we'll check if we've already // emitted an error message and if so, we'll emit all following messages as errors too. // // todo: This can lead to some really ugly output though, so we might want to look // into a more reliable way to detect errors propagating out of the compiler. If // we always wrapped rustc, then we could store this data somewhere in a much more // reliable format. if line.trim_start().starts_with("error:") { emitting_error = true; } // Note that previous text lines might have set emitting_error to true match emitting_error { true => ctx.status_build_error(line), false => ctx.status_build_message(line), } } // Here, we record Message::CompilerArtifact(artifact) => { units_compiled += 1; let target_name = artifact.target.name.clone(); ctx.status_build_progress( units_compiled, crate_count, target_name, artifact.fresh, ); output_location = artifact.executable.map(Into::into); } // todo: this can occasionally swallow errors, so we should figure out what exactly is going wrong // since that is a really bad user experience. Message::BuildFinished(finished) => { if !finished.success { bail!( "cargo build finished with errors for target: {} [{}]", self.main_target, self.triple ); } } _ => {} } } // If there's any warnings from the linker, we should print them out self.print_linker_warnings(&output_location); // Load the captured rustc args from the rustc_workspace_wrapper let workspace_rustc_args = self.load_rustc_argset()?; // Ensure the final exe exists - throw if it doesn't let exe = output_location.context("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information.")?; // Fat builds need to be linked with the fat linker. Would also like to link here for thin builds if matches!(ctx.mode, BuildMode::Fat) { self.run_fat_link(ctx, &exe, &workspace_rustc_args).await?; } // Asset extraction is starts bundle ctx.status_start_bundle(); // Extract all linker metadata (assets, Android/iOS plugins, widget extensions) in a single pass. let assets = self.collect_assets_and_metadata(&exe, ctx).await?; let time_end = SystemTime::now(); let mode = ctx.mode.clone(); let depinfo = RustcDepInfo::from_file(&exe.with_extension("d")).unwrap_or_default(); Ok(BuildArtifacts { time_end, exe, workspace_rustc: workspace_rustc_args, time_start, assets, mode, depinfo, root_dir: self.root_dir(), patch_cache: None, build_id: ctx.build_id, }) } /// with our hotpatching setup since it uses linker interception. /// /// This is sadly a hack. I think there might be other ways of busting the fingerprint (rustc wrapper?) /// but that would require relying on cargo internals. /// /// This might stop working if/when cargo stabilizes contents-based fingerprinting. /// /// `dx` compiles everything with `--target` which ends up with a structure like: /// `target///.fingerprint/-` /// /// Normally you can't rely on this structure (ie with `cargo build`) but the explicit /// target arg guarantees this will work. /// /// Each binary target has a fingerprinted location which we place under /// `target/dx/.args/name-hash.lib.json` /// /// The hash includes the various args profile info that provide entropy to disambiguate the crate. /// You might see `.args/serde-123.lib.json` in the same folder as `.args/serde-456.json` because /// they might have different config flags, different target triples, different opt levels, etc. /// Bust cargo fingerprints to force recompilation during the fat build. /// /// The tip crate is always busted so we get a fresh linker invocation. For workspace /// dependency crates, we check whether we already have cached rustc args in the scope /// directory from a previous run. If a crate's `.lib.json` exists, its args are /// still valid and we skip busting so cargo can reuse its incremental artifacts. If the /// file is missing, we bust that crate's fingerprint to force the rustc wrapper to /// re-capture its args. fn bust_fingerprint(&self, ctx: &BuildContext) -> Result<()> { // Ensure the rustc args capture directory exists - only in fat/base builds. // This ensures we always capture fresh rustc args provided we're not hotpatching. This could // be annoying for regular dx build/bundle commands, but should generally be fine. // todo: think about how CI might interact with this if it's not persisting the dx folder if matches!(ctx.mode, BuildMode::Thin { .. }) { return Ok(()); } // Always make sure the rustc arg dir is ready to receive rustc_wrapper emits _ = std::fs::create_dir_all(&self.rustc_wrapper_args_scope_dir(&ctx.mode)?) .context("Failed to create rustc wrapper args scope dir"); // Only remove args in fat mode if !matches!(ctx.mode, BuildMode::Fat) { return Ok(()); } // Always bust the tip crate. let mut bust = HashSet::new(); bust.insert(self.package().name.clone()); // Walk workspace deps of the tip crate. If we're missing cached args for any of // them, bust their fingerprint so the wrapper re-captures during this fat build. // Use the raw path (not canonicalized) since the scope dir may not exist yet. let scope_dir = self .rustc_wrapper_args_dir() .join(self.rustc_wrapper_scope_dir_name(&BuildMode::Fat)?); for dep_name in self.workspace_crate_dep_names() { if !scope_dir.join(format!("{dep_name}.lib.json")).exists() { bust.insert(dep_name); } } let fingerprint_dir = self.cargo_fingerprint_dir(); for entry in std::fs::read_dir(&fingerprint_dir) .into_iter() .flatten() .flatten() { if let Some(fname) = entry.file_name().to_str() { if let Some((name, _)) = fname.rsplit_once('-') { if bust.contains(name) { _ = std::fs::remove_dir_all(entry.path()); } } } } Ok(()) } /// Take the output of rustc and make it into the main exe of the bundle /// /// For wasm, we'll want to run `wasm-bindgen` to make it a wasm binary along with some other optimizations /// Other platforms we might do some stripping or other optimizations /// Move the executable to the workdir async fn write_executable( &self, ctx: &BuildContext, artifacts: &mut BuildArtifacts, ) -> Result<()> { match self.bundle { // Run wasm-bindgen on the wasm binary and set its output to be in the bundle folder // Also run wasm-opt on the wasm binary, and sets the index.html since that's also the "executable". // // The wasm stuff will be in a folder called "wasm" in the workdir. BundleFormat::Web => { self.bundle_web(ctx, &artifacts.exe, &mut artifacts.assets) .await?; } // this will require some extra oomf to get the multi architecture builds... // for now, we just copy the exe into the current arch (which, sorry, is hardcoded for my m1) // we'll want to do multi-arch builds in the future, so there won't be *one* exe dir to worry about // eventually `exe_dir` and `main_exe` will need to take in an arch and return the right exe path // // todo(jon): maybe just symlink this rather than copy it? // we might want to eventually use the objcopy logic to handle this // // https://github.com/rust-mobile/xbuild/blob/master/xbuild/template/lib.rs // https://github.com/rust-mobile/xbuild/blob/master/apk/src/lib.rs#L19 // // These are all super simple, just copy the exe into the folder // eventually, perhaps, maybe strip + encrypt the exe? BundleFormat::Android | BundleFormat::MacOS | BundleFormat::Windows | BundleFormat::Linux | BundleFormat::Ios | BundleFormat::Server => { std::fs::create_dir_all(self.exe_dir())?; std::fs::copy(&artifacts.exe, self.main_exe())?; } } Ok(()) } /// Bundle shared libraries into the app's frameworks folder so they sit next to the binary /// at runtime and the dynamic loader can find them. /// /// Scans the captured linker arguments for `.dylib` and `.so` paths and copies each one /// into `frameworks_folder()`. In dev builds on unix/windows the copy is a symlink so /// rebuilds don't re-copy large files; release builds do a real copy for distribution. /// /// Also handles platform-specific extras: /// - **Android**: always creates the jniLibs framework dir, copies `libc++_shared.so` /// when `-lc++_shared` appears in the link args, and copies prebuilt OpenSSL libs /// (`libssl.so`, `libcrypto.so`) when the link args reference the OpenSSL directory. /// /// Note: Windows `.dll` bundling is not yet implemented — system DLLs should not be /// bundled, and we don't yet distinguish user DLLs from system ones. async fn write_frameworks(&self, artifacts: &BuildArtifacts) -> Result<()> { let framework_dir = self.frameworks_folder(); // We have some prebuilt stuff that needs to be copied into the framework dir let openssl_dir = AndroidTools::openssl_lib_dir(&self.triple); let openssl_dir_disp = openssl_dir.display().to_string(); for arg in &artifacts.workspace_rustc.link_args { // todo - how do we handle windows dlls? we don't want to bundle the system dlls // for now, we don't do anything with dlls, and only use .dylibs and .so files // Write dylibs and dlls to the frameworks folder if arg.ends_with(".dylib") | arg.ends_with(".so") { let from = PathBuf::from(arg); let to = framework_dir.join(from.file_name().unwrap()); _ = std::fs::remove_file(&to); 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 { #[cfg(windows)] std::os::windows::fs::symlink_file(from, to).with_context(|| { "Failed to symlink framework into bundle: {from:?} -> {to:?}" })?; #[cfg(unix)] std::os::unix::fs::symlink(from, to).with_context(|| { "Failed to symlink framework into bundle: {from:?} -> {to:?}" })?; } else { std::fs::copy(from, to)?; } } // 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") { std::fs::copy( self.workspace.android_tools()?.libcpp_shared(&self.triple), framework_dir.join("libc++_shared.so"), ) .with_context(|| "Failed to copy libc++_shared.so into bundle")?; } // Copy over libssl and libcrypto if they are present in the link args if self.bundle == BundleFormat::Android && arg.contains(openssl_dir_disp.as_str()) { let libssl_source = openssl_dir.join("libssl.so"); let libcrypto_source = openssl_dir.join("libcrypto.so"); let libssl_target = framework_dir.join("libssl.so"); let libcrypto_target = framework_dir.join("libcrypto.so"); std::fs::copy(&libssl_source, &libssl_target).with_context(|| { format!("Failed to copy libssl.so into bundle\nfrom {libssl_source:?}\nto {libssl_target:?}") })?; std::fs::copy(&libcrypto_source, &libcrypto_target).with_context( || format!("Failed to copy libcrypto.so into bundle\nfrom {libcrypto_source:?}\nto {libcrypto_target:?}"), )?; } } Ok(()) } /// Collect assets and plugin metadata from the final executable in one pass /// /// This method extracts assets and FFI plugin metadata (Android/Swift) from the /// binary. Permissions are now read from Dioxus.toml, not extracted from the binary. pub async fn collect_assets_and_metadata( &self, exe: &Path, ctx: &BuildContext, ) -> Result { use super::assets::extract_symbols_from_file; let skip_assets = self.skip_assets; let needs_android_artifacts = self.bundle == BundleFormat::Android; let needs_swift_packages = matches!(self.bundle, BundleFormat::Ios | BundleFormat::MacOS); if skip_assets && !needs_android_artifacts && !needs_swift_packages { return Ok(AppManifest::new()); } ctx.status_extracting_assets(); let mut manifest = extract_symbols_from_file(exe).await?; if matches!(self.bundle, BundleFormat::Web) && matches!(ctx.mode, BuildMode::Base { .. } | BuildMode::Fat) { if let Some(dir) = self.user_public_dir() { for entry in walkdir::WalkDir::new(&dir) .into_iter() .filter_map(|e| e.ok()) .filter(|e| e.file_type().is_file()) { let from = entry.path().to_path_buf(); let relative_path = from.strip_prefix(&dir).unwrap(); let to = format!("../{}", relative_path.display()); manifest.insert_asset(BundledAsset::new( from.to_string_lossy().as_ref(), to.as_str(), manganis_core::AssetOptions::builder() .with_hash_suffix(false) .into_asset_options(), )); } } } Ok(manifest) } /// Copy the assets out of the manifest and into the target location /// /// Should be the same on all platforms - just copy over the assets from the manifest into the output directory async fn write_assets(&self, ctx: &BuildContext, assets: &AppManifest) -> Result<()> { // Server doesn't need assets - web will provide them if !ctx.is_primary_build() { return Ok(()); } let asset_dir = self.bundle_asset_dir(); // First, clear the asset dir of any files that don't exist in the new manifest _ = std::fs::create_dir_all(&asset_dir); // Create a set of all the paths that new files will be bundled to let mut keep_bundled_output_paths: HashSet<_> = assets .unique_assets() .map(|a| asset_dir.join(a.bundled_path())) .collect(); // The CLI creates a .manifest.json file in the asset dir to keep track of the assets and // other build metadata. If we can't parse this file (or the CLI version changed), then we // want to re-copy all the assets rather than trying to do an incremental update. let clear_cache = self .load_bundle_manifest() .map(|manifest| manifest.cli_version != crate::VERSION.as_str()) .unwrap_or(true); if clear_cache { keep_bundled_output_paths.clear(); } tracing::trace!( "Keeping bundled output paths: {:#?}", keep_bundled_output_paths ); // todo(jon): we also want to eventually include options for each asset's optimization and compression, which we currently aren't let mut assets_to_transfer = vec![]; // Queue the bundled assets (skip sidecar assets that require special processing) for bundled in assets.unique_assets() { let from = PathBuf::from(bundled.absolute_source_path()); let to = asset_dir.join(bundled.bundled_path()); // prefer to log using a shorter path relative to the workspace dir by trimming the workspace dir let from_ = from .strip_prefix(self.workspace_dir()) .unwrap_or(from.as_path()); let to_ = from .strip_prefix(self.workspace_dir()) .unwrap_or(to.as_path()); tracing::debug!("Copying asset {from_:?} to {to_:?}"); assets_to_transfer.push((from, to, *bundled.options())); } let asset_count = assets_to_transfer.len(); let started_processing = AtomicUsize::new(0); let copied = AtomicUsize::new(0); // Parallel Copy over the assets and keep track of progress with an atomic counter let progress = ctx.clone(); let ws_dir = self.workspace_dir(); let esbuild_path = crate::esbuild::Esbuild::path_if_installed(); // Optimizing assets is expensive and blocking, so we do it in a tokio spawn blocking task tokio::task::spawn_blocking(move || { assets_to_transfer .par_iter() .try_for_each(|(from, to, options)| { let processing = started_processing.fetch_add(1, Ordering::SeqCst); let from_ = from.strip_prefix(&ws_dir).unwrap_or(from); tracing::trace!( "Starting asset copy {processing}/{asset_count} from {from_:?}" ); let res = process_file_to(options, from, to, esbuild_path.as_deref()); if let Err(err) = res.as_ref() { tracing::error!("Failed to copy asset {from:?}: {err}"); } progress.status_copied_asset( copied.fetch_add(1, Ordering::SeqCst), asset_count, from.to_path_buf(), ); res.map(|_| ()) }) }) .await .map_err(|e| anyhow::anyhow!("A task failed while trying to copy assets: {e}"))??; // Remove the wasm dir if we packaged it to an "asset"-type app if self.should_bundle_to_asset() { _ = std::fs::remove_dir_all(self.wasm_bindgen_out_dir()); } // Write the version file so we know what version of the optimizer we used self.write_app_manifest(assets).await?; Ok(()) } /// Assemble the `cargo rustc` / `rustc` command /// /// When building fat/base binaries, we use `cargo rustc`. /// When building thin binaries, we use `rustc` directly. /// /// When processing the output of this command, you need to make sure to handle both cases which /// both have different formats (but with json output for both). fn cargo_build_command(&self, build_mode: &BuildMode) -> Result { match build_mode { // We're assembling rustc directly, so we need to be *very* careful. Cargo sets rustc's // env up very particularly, and we want to match it 1:1 but with some changes. // // To do this, we reset the env completely, and then pass every env var that the original // rustc process had 1:1. // // We need to unset a few things, like the RUSTC wrappers and then our special env var // indicating that dx itself is the compiler. If we forget to do this, then the compiler // ends up doing some recursive nonsense and dx is trying to link instead of compiling. // // todo: maybe rustc needs to be found on the FS instead of using the one in the path? BuildMode::Thin { workspace_rustc_args, .. } => { let rustc_args = workspace_rustc_args .rustc_args .get(&format!("{}.bin", self.tip_crate_name())) .context("Missing rustc args for tip crate")?; let mut cmd = Command::new("rustc"); cmd.current_dir(self.workspace_dir()); cmd.env_clear(); cmd.args(rustc_args.args[1..].iter()); cmd.env_remove("RUSTC_WORKSPACE_WRAPPER"); cmd.env_remove("RUSTC_WRAPPER"); cmd.env_remove(DX_RUSTC_WRAPPER_ENV_VAR); cmd.envs( self.cargo_build_env_vars(build_mode)? .iter() .map(|(k, v)| (k.as_ref(), v)), ); cmd.arg(format!("-Clinker={}", Workspace::path_to_dx()?.display())); if self.is_wasm_or_wasi() { cmd.arg("-Crelocation-model=pic"); } cmd.envs(rustc_args.envs.iter().cloned()); Ok(cmd) } // For Base and Fat builds, we use a regular cargo setup, but we intercept rustc for // workspace member crates to capture their args/envs for hot-patching. // // We use RUSTC_WORKSPACE_WRAPPER which wraps only workspace member crates, letting us // capture per-crate args without interfering with external dependency compilation. Note // that this will also separate dx from cargo/clippy/check because the path to dx ends up // being used in the hash check. This means dx's output is always reliable! // // We've also had a number of issues with incorrect canonicalization when passing paths // through envs on windows, hence the frequent use of dunce::canonicalize. _ => { let mut cmd = Command::new("cargo"); let env = self.cargo_build_env_vars(build_mode)?; let args = self.cargo_build_arguments(build_mode); tracing::trace!("Building with cargo rustc"); for e in env.iter() { tracing::trace!(": {}={}", e.0, e.1.to_string_lossy()); } for a in args.iter() { tracing::trace!(": {}", a); } cmd.arg("rustc") .current_dir(self.crate_dir()) .arg("--message-format") .arg("json-diagnostic-rendered-ansi") .args(args) .envs(env.iter().map(|(k, v)| (k.as_ref(), v))); // Set the folder where dx will write its captured rustc args for replay. // Use the scoped directory so different build configurations (triples, // profiles, etc.) don't collide. cmd.env( DX_RUSTC_WRAPPER_ENV_VAR, dunce::canonicalize(self.rustc_wrapper_args_scope_dir(build_mode)?) .context("Failed to canonicalize rustc wrapper args dir")?, ); // And then set the wrapper itself as dx. This will ensure both that dx captures the // rustc args and the output hashes will not conflict with non-dx tools (cargo includes // the RUSTC_WORKSPACE_WRAPPER value *in the artifact hash*) cmd.env("RUSTC_WORKSPACE_WRAPPER", Workspace::path_to_dx()?); Ok(cmd) } } } /// Create a list of arguments for cargo builds /// /// We always use `cargo rustc` *or* `rustc` directly. This means we can pass extra flags like /// `-C` arguments directly to the compiler. #[allow(clippy::vec_init_then_push)] pub(crate) fn cargo_build_arguments(&self, build_mode: &BuildMode) -> Vec { let mut cargo_args = Vec::with_capacity(4); // Set the `--config profile.{profile}.{key}={value}` flags for the profile, filling in adhoc profile cargo_args.extend(self.profile_args()); // Add required profile flags. --release overrides any custom profiles. cargo_args.push("--profile".to_string()); cargo_args.push(self.profile.to_string()); // Pass the appropriate target to cargo. We *always* specify a target which is somewhat helpful for preventing thrashing cargo_args.push("--target".to_string()); cargo_args.push(self.triple.to_string()); // We always run in verbose since the CLI itself is the one doing the presentation cargo_args.push("--verbose".to_string()); // Always push --no-default-features cargo_args.push("--no-default-features".to_string()); if self.all_features { cargo_args.push("--all-features".to_string()); } if !self.features.is_empty() { cargo_args.push("--features".to_string()); cargo_args.push(self.features.join(" ")); } // We *always* set the package since that's discovered from cargo metadata cargo_args.push(String::from("-p")); cargo_args.push(self.package.clone()); // Set the executable match self.executable_type() { TargetKind::Bin => cargo_args.push("--bin".to_string()), TargetKind::Lib => cargo_args.push("--lib".to_string()), TargetKind::Example => cargo_args.push("--example".to_string()), _ => {} }; cargo_args.push(self.executable_name().to_string()); // Set offline/locked/frozen let lock_opts = crate::verbosity_or_default(); if lock_opts.frozen { cargo_args.push("--frozen".to_string()); } if lock_opts.locked { cargo_args.push("--locked".to_string()); } if lock_opts.offline { cargo_args.push("--offline".to_string()); } // Merge in extra args. Order shouldn't really matter. cargo_args.extend(self.extra_cargo_args.clone()); cargo_args.push("--".to_string()); cargo_args.extend(self.extra_rustc_args.clone()); // On windows, we pass /SUBSYSTEM:WINDOWS to prevent a console from appearing if matches!(self.bundle, BundleFormat::Windows) && !self .rustflags .flags .iter() .any(|f| f.starts_with("-Clink-arg=/SUBSYSTEM:")) { let subsystem = self .windows_subsystem .clone() .unwrap_or_else(|| "WINDOWS".to_string()); cargo_args.push(format!("-Clink-arg=/SUBSYSTEM:{}", subsystem)); // We also need to set the entry point to mainCRTStartup to avoid windows looking // for a WinMain function cargo_args.push("-Clink-arg=/ENTRY:mainCRTStartup".to_string()); } // The bundle splitter needs relocation data to create a call-graph. // This will automatically be erased by wasm-opt during the optimization step. if self.bundle == BundleFormat::Web && self.wasm_split { cargo_args.push("-Clink-args=--emit-relocs".to_string()); } // dx links android, thin builds, and fat builds with a custom linker. // Note: We don't intercept Darwin Base builds since Swift plugins are compiled as dynamic // frameworks that load at runtime, not linked statically into the binary. let use_dx_linker = self.custom_linker.is_some() || matches!(build_mode, BuildMode::Thin { .. } | BuildMode::Fat); if use_dx_linker { cargo_args.push(format!( "-Clinker={}", Workspace::path_to_dx().expect("can't find dx").display() )); } // for debuggability, we need to make sure android studio can properly understand our build // https://stackoverflow.com/questions/68481401/debugging-a-prebuilt-shared-library-in-android-studio if self.bundle == BundleFormat::Android { cargo_args.push("-Clink-arg=-Wl,--build-id=sha1".to_string()); } // Handle frameworks/dylibs by setting the rpath // This is dependent on the bundle structure - iOS uses a flat structure while macOS uses nested // todo: we need to figure out what to do for windows match self.triple.operating_system { OperatingSystem::Darwin(_) | OperatingSystem::MacOSX { .. } => { // macOS: App.app/Contents/MacOS/exe -> ../Frameworks/ cargo_args.push("-Clink-arg=-Wl,-rpath,@executable_path/../Frameworks".to_string()); cargo_args.push("-Clink-arg=-Wl,-rpath,@executable_path".to_string()); } OperatingSystem::IOS(_) => { // iOS: App.app/exe -> Frameworks/ (flat bundle structure) cargo_args.push("-Clink-arg=-Wl,-rpath,@executable_path/Frameworks".to_string()); cargo_args.push("-Clink-arg=-Wl,-rpath,@executable_path".to_string()); } OperatingSystem::Linux => { cargo_args.push("-Clink-arg=-Wl,-rpath,$ORIGIN/../lib".to_string()); cargo_args.push("-Clink-arg=-Wl,-rpath,$ORIGIN".to_string()); } OperatingSystem::Windows => { if let Some((search_path, link_spec)) = self.winres_linker_args() { cargo_args.extend(["-L".to_string(), search_path, "-l".to_string(), link_spec]); } } _ => {} } // Our fancy hot-patching engine needs a lot of customization to work properly. // // These args are mostly intended to be passed when *fat* linking but are generally fine to // pass for both fat and thin linking. // // We need save-temps and no-dead-strip in both cases though. When we run `cargo rustc` with // these args, they will be captured and re-ran for the fast compiles in the future, so whatever // we set here will be set for all future hot patches too. if matches!(build_mode, BuildMode::Thin { .. } | BuildMode::Fat) { // rustc gives us some portable flags required: // - link-dead-code: prevents rust from passing -dead_strip to the linker since that's the default. // - save-temps=true: keeps the incremental object files around, which we need for manually linking. cargo_args.extend_from_slice(&[ "-Csave-temps=true".to_string(), "-Clink-dead-code".to_string(), ]); // We need to set some extra args that ensure all symbols make it into the final output // and that the linker doesn't strip them out. // // This basically amounts of -all_load or --whole-archive, depending on the linker. // We just assume an ld-like interface on macos and a gnu-ld interface elsewhere. // // macOS/iOS use ld64 but through the `cc` interface. // cargo_args.push("-Clink-args=-Wl,-all_load".to_string()); // // Linux and Android fit under this umbrella, both with the same clang-like entrypoint // and the gnu-ld interface. // // cargo_args.push("-Clink-args=-Wl,--whole-archive".to_string()); // // If windows -Wl,--whole-archive is required since it follows gnu-ld convention. // There might be other flags on windows - we haven't tested windows thoroughly. // // cargo_args.push("-Clink-args=-Wl,--whole-archive".to_string()); // https://learn.microsoft.com/en-us/cpp/build/reference/wholearchive-include-all-library-object-files?view=msvc-170 // // ------------------------------------------------------------ // // if web, -Wl,--whole-archive is required since it follows gnu-ld convention. // // We also use --no-gc-sections and --export-table and --export-memory to push // said symbols into the export table. // // We use --emit-relocs to build up a solid call graph. // // rust uses its own wasm-ld linker which can be found here (it's just gcc-ld with a `-target wasm` flag): // - ~/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/bin/gcc-ld // - ~/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/bin/gcc-ld/wasm-ld // // Note that we can't use --export-all, unfortunately, since some symbols are internal // to wasm-bindgen and exporting them causes the JS generation to fail. // // We are basically replicating what emscripten does here with its dynamic linking // approach where the MAIN_MODULE is very "fat" and exports the necessary arguments // for the side modules to be linked in. This guide is really helpful: // // https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md // // The tricky one is -Ctarget-cpu=mvp, which prevents rustc from generating externref // entries. // // https://blog.rust-lang.org/2024/09/24/webassembly-targets-change-in-default-target-features/#disabling-on-by-default-webassembly-proposals // // It's fine that these exist in the base module but not in the patch. if matches!( self.triple.architecture, target_lexicon::Architecture::Wasm32 | target_lexicon::Architecture::Wasm64 ) || self.triple.operating_system == OperatingSystem::Wasi { // cargo_args.push("-Ctarget-cpu=mvp".into()); // disabled due to changes in wasm-bindgne cargo_args.push("-Clink-arg=--no-gc-sections".into()); cargo_args.push("-Clink-arg=--growable-table".into()); cargo_args.push("-Clink-arg=--export-table".into()); cargo_args.push("-Clink-arg=--export-memory".into()); cargo_args.push("-Clink-arg=--emit-relocs".into()); cargo_args.push("-Clink-arg=--export=__stack_pointer".into()); cargo_args.push("-Clink-arg=--export=__heap_base".into()); cargo_args.push("-Clink-arg=--export=__data_end".into()); } } cargo_args } pub(crate) fn cargo_build_env_vars( &self, build_mode: &BuildMode, ) -> Result, OsString)>> { let mut env_vars = vec![]; // Make sure to set all the crazy android flags. Cross-compiling is hard, man. if self.bundle == BundleFormat::Android { env_vars.extend(self.android_env_vars()?); }; // Always bake the product name into the binary so bundled apps can find their assets // at runtime regardless of build profile (the asset directory structure uses the product name). env_vars.push((PRODUCT_NAME_ENV.into(), self.bundled_app_name().into())); // If this is a release build, bake the base path and title into the binary with env vars. // todo: should we even be doing this? might be better being a build.rs or something else. if self.release { if let Some(base_path) = self.trimmed_base_path() { env_vars.push((ASSET_ROOT_ENV.into(), base_path.to_string().into())); } env_vars.push(( APP_TITLE_ENV.into(), self.config.web.app.title.clone().into(), )); } // Assemble the rustflags by peering into the `.cargo/config.toml` file let rust_flags = self.rustflags.clone(); // Set the rust flags for the build if they're not empty. if !rust_flags.flags.is_empty() { env_vars.push(( "RUSTFLAGS".into(), rust_flags .encode_space_separated() .context("Failed to encode RUSTFLAGS")? .into(), )); } // If we're either zero-linking or using a custom linker, make `dx` itself do the linking. // Note: We don't intercept Darwin Base builds since Swift plugins are compiled as dynamic // frameworks that load at runtime, not linked statically into the binary. let use_dx_linker = self.custom_linker.is_some() || matches!(build_mode, BuildMode::Thin { .. } | BuildMode::Fat); if use_dx_linker { // For Android, we pass the actual linker so cargo can still link normally. // For Fat/Thin builds, we use no-link mode (linker = None). LinkAction { triple: self.triple.clone(), linker: self.custom_linker.clone(), link_err_file: dunce::canonicalize(self.link_err_file())?, link_args_file: dunce::canonicalize(self.link_args_file())?, } .write_env_vars(&mut env_vars)?; } Ok(env_vars) } /// Post-process the final binary. /// Strip the final binary after extracting all assets with rustc-objcopy async fn post_process_executable(&self, artifacts: &BuildArtifacts) -> Result<()> { // Never strip the binary if we are going to bundle split it if self.wasm_split { return Ok(()); } // Use the same format that rust itself does // https://github.com/rust-lang/rust/blob/cb80ff132a0e9aa71529b701427e4e6c243b58df/compiler/rustc_codegen_ssa/src/back/linker.rs#L1433-L1443 let strip_arg = match self.get_strip_setting() { StripSetting::Debuginfo => Some("--strip-debug"), StripSetting::Symbols => Some("--strip-all"), StripSetting::None => None, }; if let Some(strip_arg) = strip_arg { let rustc_objcopy = self.workspace.rustc_objcopy(); let dylib_path = self.workspace.rustc_objcopy_dylib_path(); // Use rustc_objcopy in place. // todo: actually use this to copy over the binary to our staging let mut command = Command::new(rustc_objcopy); command.env("LD_LIBRARY_PATH", &dylib_path); command .arg(strip_arg) .arg(&artifacts.exe) .arg(&artifacts.exe); let output = command.output().await?; if !output.status.success() { if let Ok(stdout) = std::str::from_utf8(&output.stdout) { tracing::error!("{}", stdout); } if let Ok(stderr) = std::str::from_utf8(&output.stderr) { tracing::error!("{}", stderr); } bail!("Failed to strip binary"); } } Ok(()) } /// todo(jon): use handlebars templates instead of these prebaked templates async fn write_metadata(&self) -> Result<()> { // write the Info.plist file match self.bundle { BundleFormat::MacOS => { let dest = self.root_dir().join("Contents").join("Info.plist"); let plist = self.info_plist_contents(self.bundle)?; std::fs::write(dest, plist)?; } BundleFormat::Ios => { let dest = self.root_dir().join("Info.plist"); let plist = self.info_plist_contents(self.bundle)?; std::fs::write(dest, plist)?; } // AndroidManifest.xml // er.... maybe even all the kotlin/java/gradle stuff? BundleFormat::Android => {} // Probably some custom format or a plist file (haha) // When we do the proper bundle, we'll need to do something with wix templates, I think? BundleFormat::Windows => {} // eventually we'll create the .appimage file, I guess? BundleFormat::Linux => {} // These are served as folders, not appimages, so we don't need to do anything special (I think?) // Eventually maybe write some secrets/.env files for the server? // We could also distribute them as a deb/rpm for linux and msi for windows BundleFormat::Web => {} BundleFormat::Server => {} } Ok(()) } /// Write out the manganis ffi plugins that we support /// - Kotlin / Java /// - Swift /// - TS: Todo /// - JS: Todo async fn write_ffi_plugins( &self, ctx: &BuildContext, artifacts: &BuildArtifacts, ) -> Result<()> { // Install prebuilt Android plugin artifacts (AARs + Gradle deps) if self.bundle == BundleFormat::Android && !artifacts.assets.android_artifacts.is_empty() { let names: Vec<_> = artifacts .assets .android_artifacts .iter() .map(|a| a.plugin_name.as_str().to_string()) .collect(); ctx.status_compiling_native_plugins(format!("Kotlin build: {}", names.join(", "))); self.install_android_artifacts(&artifacts.assets.android_artifacts) .context("Failed to install Android plugin artifacts")?; } if matches!(self.bundle, BundleFormat::Ios | BundleFormat::MacOS) && !artifacts.assets.swift_sources.is_empty() { let names: Vec<_> = artifacts .assets .swift_sources .iter() .map(|s| s.plugin_name.as_str().to_string()) .collect(); ctx.status_compiling_native_plugins(format!("Swift build: {}", names.join(", "))); // Compile Swift packages from source self.compile_swift_sources(&artifacts.assets.swift_sources) .await .context("Failed to compile Swift packages")?; // Then embed Swift standard libraries self.embed_swift_stdlibs(&artifacts.assets.swift_sources) .await .context("Failed to embed Swift standard libraries")?; } // Compile and install Apple Widget Extensions from Dioxus.toml config if matches!(self.bundle, BundleFormat::Ios | BundleFormat::MacOS) && !self.config.ios.widget_extensions.is_empty() { let names: Vec<_> = self .config .ios .widget_extensions .iter() .map(|w| w.display_name.clone()) .collect(); ctx.status_compiling_native_plugins(format!("Widget build: {}", names.join(", "))); self.compile_widget_extensions() .await .context("Failed to compile widget extensions")?; } Ok(()) } /// Run the optimizers, obfuscators, minimizers, signers, etc async fn optimize(&self, ctx: &BuildContext) -> Result<()> { ctx.profile_phase("Optimizing Bundle"); match self.bundle { BundleFormat::Web => { // Compress the asset dir // If pre-compressing is enabled, we can pre_compress the wasm-bindgen output let pre_compress = self.should_pre_compress_web_assets(self.release); if pre_compress { ctx.status_compressing_assets(); let asset_dir = self.bundle_asset_dir(); tokio::task::spawn_blocking(move || { crate::fastfs::pre_compress_folder(&asset_dir, pre_compress) }) .await .unwrap()?; } } BundleFormat::MacOS | BundleFormat::Windows | BundleFormat::Linux | BundleFormat::Ios | BundleFormat::Android | BundleFormat::Server => {} } Ok(()) } /// Run any final tools to produce apks or other artifacts we might need. /// /// This might include codesigning, zipping, creating an appimage, etc async fn assemble(&self, ctx: &BuildContext) -> Result<()> { ctx.profile_phase("Assembling Bundle"); if let BundleFormat::Android = self.bundle { self.assemble_android(ctx).await?; } // if the triple is a ios or macos target, we need to codesign the binary if self.is_apple_target() && self.should_codesign { self.codesign_apple(ctx).await?; } Ok(()) } /// We only really currently care about: /// /// - app dir (.app, .exe, .apk, etc) /// - assetas dir /// - exe dir (.exe, .app, .apk, etc) /// - extra scaffolding /// /// It's not guaranteed that they're different from any other folder fn prepare_build_dir(&self, ctx: &BuildContext) -> Result<()> { use std::fs::{create_dir_all, remove_dir_all}; use std::sync::OnceLock; static PRIMARY_INITIALIZED: OnceLock> = OnceLock::new(); static SECONDARY_INITIALIZED: OnceLock> = OnceLock::new(); let initializer = if ctx.is_primary_build() { &PRIMARY_INITIALIZED } else { &SECONDARY_INITIALIZED }; let success = initializer.get_or_init(|| { if ctx.is_primary_build() { _ = remove_dir_all(self.exe_dir()); } create_dir_all(self.root_dir())?; create_dir_all(self.exe_dir())?; create_dir_all(self.bundle_asset_dir())?; tracing::debug!( r#"Initialized build dirs: • root dir: {:?} • exe dir: {:?} • asset dir: {:?}"#, self.root_dir(), self.exe_dir(), self.bundle_asset_dir(), ); // we could download the templates from somewhere (github?) but after having banged my head against // cargo-mobile2 for ages, I give up with that. We're literally just going to hardcode the templates // by writing them here. if self.bundle == BundleFormat::Android { self.build_android_app_dir()?; } Ok(()) }); if let Err(e) = success.as_ref() { bail!("Failed to initialize build directory: {e}"); } Ok(()) } pub(crate) fn load_bundle_manifest(&self) -> Result { let manifest_path = self.bundle_manifest_file(); let manifest_data = std::fs::read_to_string(&manifest_path) .with_context(|| format!("Failed to read manifest at {:?}", &manifest_path))?; let manifest: AppManifest = serde_json::from_str(&manifest_data) .with_context(|| format!("Failed to parse manifest at {:?}", &manifest_path))?; Ok(manifest) } /// Check for tooling that might be required for this build. /// /// This should generally be only called on the first build since it takes time to verify the tooling /// is in place, and we don't want to slow down subsequent builds. pub(crate) async fn verify_tooling(&self, ctx: &BuildContext) -> Result<()> { ctx.profile_phase("Verify Tooling"); ctx.status_installing_tooling(); self.verify_toolchain_installed().await?; match self.bundle { BundleFormat::Web => self.verify_web_tooling().await?, BundleFormat::Ios => self.verify_ios_tooling().await?, BundleFormat::Android => self.verify_android_tooling().await?, BundleFormat::Linux => self.verify_linux_tooling().await?, BundleFormat::MacOS | BundleFormat::Windows | BundleFormat::Server => {} } Ok(()) } async fn verify_toolchain_installed(&self) -> Result<()> { let toolchain_dir = self.workspace.sysroot.join("lib/rustlib"); let triple = self.triple.to_string(); // Install target using rustup. if !toolchain_dir.join(&triple).exists() { tracing::info!( "{} platform requires {} to be installed. Installing...", self.bundle, triple ); let mut child = tokio::process::Command::new("rustup") .args(["target", "add"]) .arg(&triple) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .kill_on_drop(true) .spawn()?; let stdout = tokio::io::BufReader::new(child.stdout.take().unwrap()); let stderr = tokio::io::BufReader::new(child.stderr.take().unwrap()); let mut stdout_lines = stdout.lines(); let mut stderr_lines = stderr.lines(); loop { tokio::select! { line = stdout_lines.next_line() => { match line { Ok(Some(line)) => tracing::info!("{}", line), Err(err) => tracing::error!("{}", err), Ok(_) => break, } } line = stderr_lines.next_line() => { match line { Ok(Some(line)) => tracing::info!("{}", line), Err(err) => tracing::error!("{}", err), Ok(_) => break, } } } } } // Ensure target is installed. if !toolchain_dir.join(&triple).exists() { bail!("Missing rust target {}", triple); } Ok(()) } async fn write_app_manifest(&self, manifest: &AppManifest) -> Result<()> { std::fs::write( self.bundle_manifest_file(), serde_json::to_string_pretty(&manifest)?, )?; Ok(()) } async fn fill_caches(&self, ctx: &BuildContext, artifacts: &mut BuildArtifacts) -> Result<()> { // Populate the patch cache if we're in fat mode if matches!(ctx.mode, BuildMode::Fat) { ctx.profile_phase("Creating Patch Cache"); let patch_exe = match self.bundle { BundleFormat::Web => self.wasm_bindgen_wasm_output_file(), _ => artifacts.exe.to_path_buf(), }; let hotpatch_module_cache = HotpatchModuleCache::new(&patch_exe, &self.triple)?; artifacts.patch_cache = Some(Arc::new(hotpatch_module_cache)); } Ok(()) } /// Recursively copy a directory and its contents. #[allow(clippy::only_used_in_recursion)] pub(crate) fn copy_build_dir_recursive(&self, src: &Path, dst: &Path) -> Result<()> { std::fs::create_dir_all(dst)?; for entry in std::fs::read_dir(src)? { let entry = entry?; let src_path = entry.path(); let dst_path = dst.join(entry.file_name()); if src_path.is_dir() { // Skip build directories and hidden folders let name = entry.file_name(); let name_str = name.to_string_lossy(); if name_str == "build" || name_str == ".gradle" || name_str.starts_with('.') { continue; } self.copy_build_dir_recursive(&src_path, &dst_path)?; } else { std::fs::copy(&src_path, &dst_path)?; } } Ok(()) } /// Ensure the right dependencies are installed for linux apps. /// This varies by distro, so we just do nothing for now. /// /// Eventually, we want to check for the prereqs for wry/tao as outlined by tauri: /// async fn verify_linux_tooling(&self) -> Result<()> { Ok(()) } /// Get an estimate of the number of units in the crate. If nightly rustc is not available, this /// will return an estimate of the number of units in the crate based on cargo metadata. /// /// TODO: always use once it is stable async fn get_unit_count_estimate(&self, build_mode: &BuildMode) -> usize { // Try to get it from nightly if let Ok(count) = self.get_unit_count(build_mode).await { return count; } // Otherwise, use cargo metadata let units = self .workspace .krates .krates_filtered(krates::DepKind::Dev) .iter() .map(|k| k.targets.len()) .sum::(); (units as f64 / 3.5) as usize } /// Try to get the unit graph for the crate. This is a nightly only feature which may not be /// available with the current version of rustc the user has installed. /// /// It also might not be super reliable - I think in practice it occasionally returns 2x the units. async fn get_unit_count(&self, build_mode: &BuildMode) -> crate::Result { #[derive(Debug, Deserialize)] struct UnitGraph { units: Vec, } let output = tokio::process::Command::new("cargo") .arg("+nightly") .arg("rustc") .arg("--unit-graph") .arg("-Z") .arg("unstable-options") .args(self.cargo_build_arguments(build_mode)) .envs( self.cargo_build_env_vars(build_mode)? .iter() .map(|(k, v)| (k.as_ref(), v)), ) .output() .await?; if !output.status.success() { tracing::trace!( "Failed to get unit count: {}", String::from_utf8_lossy(&output.stderr) ); bail!("Failed to get unit count"); } let output_text = String::from_utf8(output.stdout).context("Failed to get unit count")?; let graph: UnitGraph = serde_json::from_str(&output_text).context("Failed to get unit count")?; Ok(graph.units.len()) } fn print_linker_warnings(&self, exe_output_location: &Option) { if let Ok(linker_warnings) = std::fs::read_to_string(self.link_err_file()) { if !linker_warnings.is_empty() { if exe_output_location.is_none() { tracing::error!("Linker warnings: {}", linker_warnings); } else { tracing::debug!("Linker warnings: {}", linker_warnings); } } } } /// Load per-crate rustc args from the wrapper directory. /// /// Each workspace crate compiled through the wrapper has its own JSON file: /// - "{crate_name}.lib.json" (key: "{crate_name}.lib") for lib targets and /// - "{crate_name}.bin.json" (key: "{crate_name}.bin") for bin targets. fn load_rustc_argset(&self) -> Result { let link_args = std::fs::read_to_string(self.link_args_file()) .context("Failed to read link args from file")? .lines() .map(|s| s.to_string()) .collect(); let mut workspace_rustc_args = WorkspaceRustcArgs::new(link_args); // Always read from the fat build's scope dir — the rustc wrapper only captures // args during fat/base builds, not thin builds. let args_dir = self.rustc_wrapper_args_scope_dir(&BuildMode::Fat)?; if let Ok(entries) = std::fs::read_dir(&args_dir) { for entry in entries.flatten() { let path = entry.path(); if path.extension().is_some_and(|e| e == "json") { if let Ok(contents) = std::fs::read_to_string(&path) { if let Ok(args) = serde_json::from_str::(&contents) { if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { workspace_rustc_args .rustc_args .insert(stem.to_string(), args); } } } } } } Ok(workspace_rustc_args) } pub(crate) fn all_target_features(&self) -> Vec { let mut features = self.features.clone(); features.dedup(); features } /// Checks the strip setting for the package, resolving profiles recursively pub(crate) fn get_strip_setting(&self) -> StripSetting { let cargo_toml = &self.workspace.cargo_toml; let profile = &self.profile; let release = self.release; let profile = match (cargo_toml.profile.custom.get(profile), release) { (Some(custom_profile), _) => Some(custom_profile), (_, true) => cargo_toml.profile.release.as_ref(), (_, false) => cargo_toml.profile.dev.as_ref(), }; let Some(profile) = profile else { return StripSetting::None; }; // Get the strip setting from the profile or the profile it inherits from fn get_strip(profile: &Profile, profiles: &Profiles) -> Option { profile.strip.as_ref().copied().or_else(|| { // If we can't find the strip setting, check if we inherit from another profile profile.inherits.as_ref().and_then(|inherits| { let profile = match inherits.as_str() { "dev" => profiles.dev.as_ref(), "release" => profiles.release.as_ref(), "test" => profiles.test.as_ref(), "bench" => profiles.bench.as_ref(), other => profiles.custom.get(other), }; profile.and_then(|p| get_strip(p, profiles)) }) }) } let Some(strip) = get_strip(profile, &cargo_toml.profile) else { // If the profile doesn't have a strip option, return None return StripSetting::None; }; strip } /// returns the path to root build folder. This will be our working directory for the build. /// /// we only add an extension to the folders where it sorta matters that it's named with the extension. /// for example, on mac, the `.app` indicates we can `open` it and it pulls in icons, dylibs, etc. /// /// for our simulator-based platforms, this is less important since they need to be zipped up anyways /// to run in the simulator. /// /// For windows/linux, it's also not important since we're just running the exe directly out of the folder /// /// The idea of this folder is that we can run our top-level build command against it and we'll get /// a final build output somewhere. Some platforms have basically no build command, and can simply /// be ran by executing the exe directly. pub(crate) fn root_dir(&self) -> PathBuf { let platform_dir = self.platform_dir(); match self.bundle { BundleFormat::Web => platform_dir.join("public"), BundleFormat::Server => platform_dir.clone(), // ends up *next* to the public folder // These might not actually need to be called `.app` but it does let us run these with `open` BundleFormat::MacOS => platform_dir.join(format!("{}.app", self.bundled_app_name())), BundleFormat::Ios => platform_dir.join(format!("{}.app", self.bundled_app_name())), // in theory, these all could end up directly in the root dir BundleFormat::Android => platform_dir.join("app"), // .apk (after bundling) BundleFormat::Linux => platform_dir.join("app"), // .appimage (after bundling) BundleFormat::Windows => platform_dir.join("app"), // .exe (after bundling) } } /// Create a workdir for the given platform /// This can be used as a temporary directory for the build, but in an observable way such that /// you can see the files in the directory via `target` /// /// target/dx/build/app/web/ /// target/dx/build/app/web/public/ /// target/dx/build/app/web/server.exe pub(crate) fn platform_dir(&self) -> PathBuf { self.internal_out_dir() .join(&self.main_target) .join(if self.release { "release" } else { "debug" }) .join(self.bundle.build_folder_name()) } pub(crate) fn platform_exe_name(&self) -> String { match self.bundle { // mac/ios are unixy and dont have an exe extension BundleFormat::MacOS | BundleFormat::Ios => self.executable_name().to_string(), // the server binary is always called "server" to avoid antivirus issues when the // binary name changes between builds (the folder name already identifies the project) BundleFormat::Server => match self.triple.operating_system { OperatingSystem::Windows => "server.exe".to_string(), _ => "server".to_string(), }, BundleFormat::Windows => match self.triple.operating_system { OperatingSystem::Windows => format!("{}.exe", self.executable_name()), _ => self.executable_name().to_string(), }, // from the apk spec, the root exe is a shared library // defaults to "main" (libmain.so) per NativeActivity convention, overridable in Dioxus.toml BundleFormat::Android => { let lib_name = self.android_lib_name(); format!("lib{lib_name}.so") } // this will be wrong, I think, but not important? BundleFormat::Web => format!("{}_bg.wasm", self.executable_name()), // todo: maybe this should be called AppRun? BundleFormat::Linux => self.executable_name().to_string(), } } /// Get the directory where this app can write to for this session that's guaranteed to be stable /// for the same app. This is useful for emitting state like window position and size. /// /// The directory is specific for this app and might be pub(crate) fn session_cache_dir(&self) -> PathBuf { self.session_cache_dir.join(self.bundle.to_string()) } pub(crate) fn rustc_wrapper_args_dir(&self) -> PathBuf { self.target_dir.join("dx").join(".captured-args") } pub(crate) fn rustc_wrapper_args_scope_dir(&self, build_mode: &BuildMode) -> Result { Ok(self .rustc_wrapper_args_dir() .join(self.rustc_wrapper_scope_dir_name(build_mode)?)) } /// The crate name that rustc uses for the tip crate (hyphens replaced with underscores). pub(crate) fn tip_crate_name(&self) -> String { self.main_target.replace('-', "_") } /// The workspace package name of the tip crate (hyphens replaced with underscores). /// /// This can differ from `tip_crate_name()` when the binary target is named differently /// than its package (e.g. package `browser` with `[[bin]] name = "blitz"`). Use this for /// workspace-graph lookups (which are keyed by package name) and `tip_crate_name()` for /// rustc `--crate-name` keys like `{name}.bin`. pub(crate) fn tip_package_name(&self) -> String { self.package().name.replace('-', "_") } /// Stderr captured from the linker during the last build. Written by the linker /// interception in `rustcwrapper` and read back to surface warnings/errors to the user. fn link_err_file(&self) -> PathBuf { self.session_cache_dir().join("link_err.txt") } /// The linker arguments captured from the tip crate's final link invocation. /// Used to replay the link step during thin (hotpatch) builds. fn link_args_file(&self) -> PathBuf { self.session_cache_dir().join("link_args.json") } /// A response file for MSVC's `link.exe`. Windows command lines have a ~32k character /// limit, so we write linker arguments to this file and pass `@` instead. pub(crate) fn windows_command_file(&self) -> PathBuf { self.session_cache_dir().join("windows_command.txt") } /// Get the outdir specified by the Dioxus.toml, relative to the crate directory. /// We don't support workspaces yet since that would cause a collision of bundles per project. pub(crate) fn crate_out_dir(&self) -> Option { self.config .application .out_dir .as_ref() .map(|out_dir| self.crate_dir().join(out_dir)) } /// Compose an out directory. Represents the typical "dist" directory that /// is "distributed" after building an application (configurable in the /// `Dioxus.toml`). fn internal_out_dir(&self) -> PathBuf { let dir = self.target_dir.join("dx"); std::fs::create_dir_all(&dir).unwrap(); dir } /// target/dx/bundle/app/ /// target/dx/bundle/app/blah.app /// target/dx/bundle/app/blah.exe /// target/dx/bundle/app/public/ pub(crate) fn bundle_dir(&self, bundle: BundleFormat) -> PathBuf { self.internal_out_dir() .join(&self.main_target) .join("bundle") .join(bundle.build_folder_name()) } /// Get the workspace directory for the crate pub(crate) fn workspace_dir(&self) -> PathBuf { self.workspace .krates .workspace_root() .as_std_path() .to_path_buf() } /// Get the directory of the crate pub(crate) fn crate_dir(&self) -> PathBuf { self.package() .manifest_path .parent() .unwrap() .as_std_path() .to_path_buf() } /// Get the package we are currently in pub(crate) fn package(&self) -> &krates::cm::Package { &self.workspace.krates[self.crate_package] } /// Get the name of the package we are compiling pub(crate) fn executable_name(&self) -> &str { &self.crate_target.name } /// Get the type of executable we are compiling pub(crate) fn executable_type(&self) -> TargetKind { self.crate_target.kind[0].clone() } pub(crate) fn bundled_app_name(&self) -> String { use convert_case::{Case, Casing}; self.executable_name().to_case(Case::Pascal) } /// Get the crate version from Cargo.toml (e.g., "0.1.0") pub(crate) fn crate_version(&self) -> String { self.workspace.krates[self.crate_package] .version .to_string() } /// Get the Android `versionCode` for the generated Gradle project. /// /// Resolution order: `--version-code` / `DX_ANDROID_VERSION_CODE`, then /// `[android] version_code` in Dioxus.toml, then a value derived from the crate /// version as `major * 1000000 + minor * 1000 + patch`. pub(crate) fn android_version_code(&self) -> Result { /// The maximum value accepted by the Google Play Store. const MAX_VERSION_CODE: u32 = 2_100_000_000; let code = match self.version_code.or(self.config.android.version_code) { Some(code) => code, None => { let version = &self.workspace.krates[self.crate_package].version; if version.major > 2099 || version.minor > 999 || version.patch > 999 { bail!( "Cannot derive an Android versionCode from crate version `{version}`: each component must fit `major <= 2099`, `minor <= 999`, `patch <= 999`. Set an explicit version code via `[android] version_code` in Dioxus.toml or the `--version-code` flag." ); } if !version.pre.is_empty() { tracing::warn!( "Pre-release identifier of crate version `{version}` is ignored when deriving the Android versionCode" ); } let code = (version.major * 1_000_000 + version.minor * 1_000 + version.patch) as u32; // Google Play requires versionCode >= 1, so bump 0.0.0 up to 1. code.max(1) } }; if !(1..=MAX_VERSION_CODE).contains(&code) { bail!( "Android versionCode must be between 1 and {MAX_VERSION_CODE}, got {code}. See https://developer.android.com/studio/publish/versioning" ); } Ok(code) } /// The explicitly-configured `CFBundleVersion` for iOS/macOS bundles, if any. /// /// Resolution order: `--bundle-version` / `DX_BUNDLE_VERSION`, then the platform's /// `bundle_version` in Dioxus.toml (`[ios]` or `[macos]`). pub(crate) fn apple_bundle_version_override(&self) -> Option { self.bundle_version.clone().or_else(|| match self.bundle { BundleFormat::Ios => self.config.ios.bundle_version.clone(), BundleFormat::MacOS => self.config.macos.bundle_version.clone(), _ => None, }) } /// The `CFBundleVersion` for iOS/macOS bundles, falling back to the crate version. pub(crate) fn apple_bundle_version(&self) -> String { self.apple_bundle_version_override() .unwrap_or_else(|| self.crate_version()) } pub(crate) fn bundle_identifier(&self) -> String { use crate::config::BundlePlatform; // Check platform-specific identifier override first, then fall back to base bundle let platform: BundlePlatform = self.bundle.into(); if let Some(identifier) = self.config.resolved_identifier(platform) { let identifier = identifier.to_string(); if identifier.contains('.') && !identifier.starts_with('.') && !identifier.ends_with('.') && !identifier.contains("..") { return identifier; } else { tracing::error!( "Invalid bundle identifier: {identifier:?}. Must contain at least one '.' and not start/end with '.'. E.g. `com.example.app`" ); } } format!("com.example.{}", self.bundled_app_name()) } /// The item that we'll try to run directly if we need to. /// /// todo(jon): we should name the app properly instead of making up the exe name. It's kinda okay for dev mode, but def not okay for prod pub(crate) fn main_exe(&self) -> PathBuf { self.exe_dir().join(self.platform_exe_name()) } pub(crate) fn is_wasm_or_wasi(&self) -> bool { matches!( self.triple.architecture, target_lexicon::Architecture::Wasm32 | target_lexicon::Architecture::Wasm64 ) || self.triple.operating_system == target_lexicon::OperatingSystem::Wasi } /// Does the app specify: /// /// - Dioxus with "fullstack" enabled? (access to serverfns, etc) /// - An explicit "fullstack" feature that enables said feature? /// /// Note that we don't detect if dependencies enable it transitively since we want to be explicit about it. /// /// The intention here is to detect if "fullstack" is enabled in the target's features list: /// ```toml /// [dependencies] /// dioxus = { version = "0.4", features = ["fullstack"] } /// ``` /// /// or as an explicit feature in default: /// ```toml /// [features] /// default = ["dioxus/fullstack"] /// ``` /// /// or as a default feature that enables the dioxus feature: /// ```toml /// [features] /// default = ["fullstack"] /// fullstack = ["dioxus/fullstack"] /// ``` /// /// or as an explicit feature (that enables the dioxus feature): /// ``` /// dx serve app --features "fullstack" /// ``` pub(crate) fn fullstack_feature_enabled(&self) -> bool { let dioxus_dep = self .package() .dependencies .iter() .find(|dep| dep.name == "dioxus"); // If we don't have a dioxus dependency, we can't be fullstack. This shouldn't impact non-dioxus projects let Some(dioxus_dep) = dioxus_dep else { return false; }; // Check if the dioxus dependency has the "fullstack" feature enabled if dioxus_dep.features.iter().any(|f| f == "fullstack") { return true; } // Check if any of the features in our feature list enables a feature that enables "fullstack" let transitive = self .package() .features .iter() .filter(|(_name, list)| list.iter().any(|f| f == "dioxus/fullstack")); for (name, _list) in transitive { if self.features.contains(name) { return true; } } false } pub(crate) fn bundle_asset_dir(&self) -> PathBuf { match self.bundle { BundleFormat::MacOS => self .root_dir() .join("Contents") .join("Resources") .join("assets"), BundleFormat::Android => self .root_dir() .join("app") .join("src") .join("main") .join("assets"), // We put assets in public/assets for server apps BundleFormat::Server => self.root_dir().join("public").join("assets"), // everyone else is soooo normal, just app/assets :) BundleFormat::Web | BundleFormat::Ios | BundleFormat::Windows | BundleFormat::Linux => { self.root_dir().join("assets") } } } /// The directory in which we'll put the main exe /// /// Mac, Android, Web are a little weird /// - mac wants to be in Contents/MacOS /// - android wants to be in jniLibs/arm64-v8a (or others, depending on the platform / architecture) /// - web wants to be in wasm (which... we don't really need to, we could just drop the wasm into public and it would work) /// /// I think all others are just in the root folder /// /// todo(jon): investigate if we need to put .wasm in `wasm`. It kinda leaks implementation details, which ideally we don't want to do. fn exe_dir(&self) -> PathBuf { match self.bundle { BundleFormat::MacOS => self.root_dir().join("Contents").join("MacOS"), BundleFormat::Web => self.root_dir().join("wasm"), // Android has a whole build structure to it BundleFormat::Android => self .root_dir() .join("app") .join("src") .join("main") .join("jniLibs") .join(AndroidTools::android_jnilib(&self.triple)), // these are all the same, I think? BundleFormat::Windows | BundleFormat::Linux | BundleFormat::Ios | BundleFormat::Server => self.root_dir(), } } /// Get the path to the app manifest file /// /// This includes metadata about the build such as the bundle format, target triple, features, etc. /// Manifests are only written by the `PRIMARY` build. fn bundle_manifest_file(&self) -> PathBuf { self.platform_dir().join(".manifest.json") } /// Blow away the fingerprint for this package, forcing rustc to recompile it. /// /// This prevents rustc from using the cached version of the binary, which can cause issues /// Find workspace crates that directly depend on the given crate. /// /// Returns underscore-normalized crate names of workspace members that have `crate_name` /// as a dependency. Used for cascade detection — when a dep's public symbols change, /// its dependents need recompilation too. pub(crate) fn workspace_dependents_of(&self, crate_name: &str) -> Vec { let krates = &self.workspace.krates; // Find the NodeId for the target crate let target_nid = krates.workspace_members().find_map(|member| { if let krates::Node::Krate { id, krate, .. } = member { if krate.name.replace('-', "_") == crate_name { return krates.nid_for_kid(id); } } None }); let Some(target_nid) = target_nid else { return Vec::new(); }; // Use krates' direct_dependents to find reverse deps, filter to workspace members let workspace_names: HashSet = krates .workspace_members() .filter_map(|m| { if let krates::Node::Krate { krate, .. } = m { Some(krate.name.replace('-', "_")) } else { None } }) .collect(); krates .direct_dependents(target_nid) .into_iter() .filter_map(|dep| { let name = dep.krate.name.replace('-', "_"); if workspace_names.contains(&name) { Some(name) } else { None } }) .collect() } /// Get the folder where Apple Widget Extensions (.appex bundles) are installed. /// This is only applicable to iOS and macOS bundles. pub(crate) fn plugins_folder(&self) -> PathBuf { match self.triple.operating_system { OperatingSystem::Darwin(_) | OperatingSystem::MacOSX(_) => { self.root_dir().join("Contents").join("PlugIns") } OperatingSystem::IOS(_) => self.root_dir().join("PlugIns"), _ => self.root_dir().join("PlugIns"), } } pub(crate) fn frameworks_folder(&self) -> PathBuf { match self.triple.operating_system { OperatingSystem::Darwin(_) | OperatingSystem::MacOSX(_) => { self.root_dir().join("Contents").join("Frameworks") } OperatingSystem::IOS(_) => self.root_dir().join("Frameworks"), OperatingSystem::Linux if self.bundle == BundleFormat::Android => { let arch = match self.triple.architecture { Architecture::Aarch64(_) => "arm64-v8a", Architecture::Arm(_) => "armeabi-v7a", Architecture::X86_32(_) => "x86", Architecture::X86_64 => "x86_64", _ => panic!( "Unsupported architecture for Android: {:?}", self.triple.architecture ), }; self.root_dir() .join("app") .join("src") .join("main") .join("jniLibs") .join(arch) } OperatingSystem::Linux | OperatingSystem::Windows => self.root_dir(), _ => self.root_dir(), } } /// Resolve the configured public directory relative to the crate, if any. pub(crate) fn user_public_dir(&self) -> Option { let path = self.config.application.public_dir.as_ref()?; if path.as_os_str().is_empty() { return None; } Some(if path.is_absolute() { path.clone() } else { self.crate_dir().join(path) }) } /// Get the base path from the config or None if this is not a web or server build pub(crate) fn base_path(&self) -> Option<&str> { self.base_path .as_deref() .or(self.config.web.app.base_path.as_deref()) .filter(|_| matches!(self.bundle, BundleFormat::Web | BundleFormat::Server)) } /// Get the normalized base path for the application with `/` trimmed from both ends. pub(crate) fn trimmed_base_path(&self) -> Option<&str> { self.base_path() .map(|p| p.trim_matches('/')) .filter(|p| !p.is_empty()) } /// Get the trimmed base path or `.` if no base path is set pub(crate) fn base_path_or_default(&self) -> &str { self.trimmed_base_path().unwrap_or(".") } /// Get the path to the package manifest directory pub(crate) fn package_manifest_dir(&self) -> PathBuf { self.workspace.krates[self.crate_package] .manifest_path .parent() .unwrap() .to_path_buf() .into() } pub(crate) async fn start_simulators(&self) -> Result<()> { if self.device_name.is_some() { return Ok(()); } match self.bundle { // Boot an iOS simulator if one is not already running. // // We always choose the most recently opened simulator based on the xcrun list. // Note that simulators can be running but the simulator app itself is not open. // Calling `open::that` is always fine, even on running apps, since apps are singletons. BundleFormat::Ios => self.start_ios_sim().await?, BundleFormat::Android => self.start_android_sim()?, // nothing - maybe on the web we should open the browser? _ => {} }; Ok(()) } /// Assemble a series of `--config key=value` arguments for the build command. /// /// This adds adhoc profiles that dx uses to isolate builds from each other. Normally if you ran /// `cargo build --feature desktop` and `cargo build --feature server`, then both binaries get /// the same name and overwrite each other, causing thrashing and locking issues. /// /// By creating adhoc profiles, we can ensure that each build is isolated and doesn't interfere with each other. /// /// The user can also define custom profiles in their `Cargo.toml` file, which will be used instead /// of the adhoc profiles. /// /// The names of the profiles are: /// - web-dev /// - web-release /// - desktop-dev /// - desktop-release /// - server-dev /// - server-release /// - ios-dev /// - ios-release /// - android-dev /// - android-release /// - liveview-dev /// - liveview-release /// /// Note how every platform gets its own profile, and each platform has a dev and release profile. fn profile_args(&self) -> Vec { // Always disable stripping so symbols still exist for the asset system. We will apply strip manually // after assets are built let profile = self.profile.as_str(); let mut args = Vec::new(); args.push(format!(r#"profile.{profile}.strip=false"#)); // If the user defined the profile in the Cargo.toml, we don't need to add it to our adhoc list if !self .workspace .cargo_toml .profile .custom .contains_key(&self.profile) { // Otherwise, we need to add the profile arguments to make it adhoc let inherits = if self.release { "release" } else { "dev" }; // Add the profile definition first. args.push(format!(r#"profile.{profile}.inherits="{inherits}""#)); // The default dioxus experience is to lightly optimize the web build, both in debug and release // Note that typically in release builds, you would strip debuginfo, but we actually choose to do // that with wasm-opt tooling instead. if matches!(self.bundle, BundleFormat::Web) { if self.release { args.push(format!(r#"profile.{profile}.opt-level="s""#)); } if self.wasm_split { args.push(format!(r#"profile.{profile}.lto=true"#)); args.push(format!(r#"profile.{profile}.debug=true"#)); } } } // Prepend --config to each argument args.into_iter() .flat_map(|arg| ["--config".to_string(), arg]) .collect() } fn cargo_fingerprint_dir(&self) -> PathBuf { self.target_dir .join(self.triple.to_string()) .join(&self.profile) .join(".fingerprint") } fn is_apple_target(&self) -> bool { matches!( self.triple.operating_system, OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) ) } /// All workspace crate names that the tip crate transitively depends on /// (underscore-normalized, excluding the tip itself). fn workspace_crate_dep_names(&self) -> Vec { let krates = &self.workspace.krates; let workspace_names: HashSet = krates .workspace_members() .filter_map(|m| match m { krates::Node::Krate { krate, .. } => Some(krate.name.replace('-', "_")), _ => None, }) .collect(); let tip = self.tip_package_name(); let Some(tip_nid) = krates.workspace_members().find_map(|m| match m { krates::Node::Krate { id, krate, .. } if krate.name.replace('-', "_") == tip => { krates.nid_for_kid(id) } _ => None, }) else { return Vec::new(); }; // BFS forward through workspace deps. let mut visited = HashSet::new(); let mut queue = VecDeque::from([tip_nid]); let mut deps = Vec::new(); while let Some(nid) = queue.pop_front() { for dep in krates.direct_dependencies(nid) { let name = dep.krate.name.replace('-', "_"); if workspace_names.contains(&name) && visited.insert(dep.node_id) { deps.push(name); queue.push_back(dep.node_id); } } } deps } }