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};
#[derive(Clone)]
pub(crate) struct BuildRequest {
pub(crate) workspace: Arc<Workspace>,
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<String>,
pub(crate) should_codesign: bool,
pub(crate) package: String,
pub(crate) main_target: String,
pub(crate) features: Vec<String>,
pub(crate) rustflags: cargo_config2::Flags,
pub(crate) extra_cargo_args: Vec<String>,
pub(crate) extra_rustc_args: Vec<String>,
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<PathBuf>,
pub(crate) base_path: Option<String>,
pub(crate) using_dioxus_explicitly: bool,
pub(crate) apple_entitlements: Option<PathBuf>,
pub(crate) apple_team_id: Option<String>,
pub(crate) session_cache_dir: PathBuf,
pub(crate) raw_json_diagnostics: bool,
pub(crate) windows_subsystem: Option<String>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, PartialEq)]
pub enum BuildMode {
Base { run: bool },
Fat,
Thin {
changed_files: Vec<PathBuf>,
aslr_reference: u64,
workspace_rustc_args: WorkspaceRustcArgs,
modified_crates: HashSet<String>,
cache: Arc<HotpatchModuleCache>,
},
}
#[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<Arc<HotpatchModuleCache>>,
pub(crate) depinfo: RustcDepInfo,
pub(crate) build_id: BuildId,
}
impl BuildRequest {
pub(crate) async fn new(args: &TargetArgs, workspace: Arc<Workspace>) -> Result<Self> {
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());
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::<Vec<_>>();
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();
let config = workspace
.load_dioxus_config(crate_package, Some(crate_target.src_path.as_std_path()))?
.unwrap_or_default();
let device = args.device.clone();
let using_dioxus_explicitly = main_package
.dependencies
.iter()
.any(|dep| dep.name == "dioxus");
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;
let dioxus_direct_renderer = Self::renderer_enabled_by_dioxus_dependency(main_package);
let known_features_as_renderers = Self::features_that_enable_renderers(main_package);
let enabled_renderers = if no_default_features {
vec![]
} else {
Self::enabled_cargo_toml_default_features_renderers(main_package)
};
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 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(|| {
let non_server_features = known_features_as_renderers
.iter()
.filter(|f| f.1.as_str() != "server")
.collect::<Vec<_>>();
if non_server_features.len() == 1 {
Some(non_server_features[0].clone())
} else {
None
}
});
if 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));
}
}
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() {
true => triple = triple.or(Some("aarch64-apple-ios".parse()?)),
false if matches!(Triple::host().architecture, Architecture::Aarch64(_)) => {
triple = triple.or(Some("aarch64-apple-ios-sim".parse()?))
}
_ => 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));
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 !no_default_features {
features.extend(Self::rendererless_features(main_package));
features.dedup();
features.sort();
}
let triple = if using_dioxus_explicitly {
triple.context("Could not automatically detect target triple")?
} else {
triple.unwrap_or(Triple::host())
};
let bundle = if using_dioxus_explicitly {
bundle_format.context("Could not automatically detect bundle format")?
} else {
bundle_format.unwrap_or(BundleFormat::host())
};
if let Some(renderer) = renderer {
if let Some(feature) =
Self::feature_for_platform_and_renderer(main_package, &triple, renderer)
{
features.push(feature);
features.dedup();
}
}
let profile = match args.profile.clone() {
Some(profile) => profile,
None => bundle.profile_name(args.release),
};
let should_codesign =
args.codesign || device.is_some() || args.apple_entitlements.is_some();
let release = workspace.is_release_profile(&profile);
let package = args
.package
.clone()
.unwrap_or_else(|| main_package.name.clone());
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();
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);
}
}
if let Ok(target) = cargo_config.target(triple.to_string()) {
if let Some(flags) = target.rustflags {
rustflags.flags.extend(flags.flags);
}
}
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()
),
]);
}
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 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()),
]);
}
}
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 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 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(),
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");
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(());
}
_ = crate::TailwindCli::run_once(
self.package_manifest_dir(),
self.config.application.tailwind_input.clone(),
self.config.application.tailwind_output.clone(),
)
.await;
if self.bundle == BundleFormat::Android {
AndroidTools::unpack_prebuilt_openssl()?;
}
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<BuildArtifacts> {
match &ctx.mode {
BuildMode::Thin { .. } => self.compile_workspace_hotpatch(&ctx).await,
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)
}
}
}
pub async fn cargo_build(&self, ctx: &BuildContext) -> Result<BuildArtifacts> {
let time_start = SystemTime::now();
_ = self.bust_fingerprint(ctx);
let crate_count = match ctx.mode {
BuildMode::Thin { .. } => 1,
_ => self.get_unit_count_estimate(&ctx.mode).await,
};
let mut child = self
.cargo_build_command(&ctx.mode)?
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("Failed to spawn cargo 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<PathBuf> = 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 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) => {
#[derive(Deserialize)]
struct RustcArtifact {
artifact: PathBuf,
emit: String,
}
if let Ok(artifact) = serde_json::from_str::<RustcArtifact>(&line) {
if artifact.emit == "link" {
output_location = Some(artifact.artifact);
}
}
if let Ok(diag) = serde_json::from_str::<Diagnostic>(&line) {
ctx.status_build_diagnostic(diag);
}
if line.trim_start().starts_with("error:") {
emitting_error = true;
}
match emitting_error {
true => ctx.status_build_error(line),
false => ctx.status_build_message(line),
}
}
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);
}
Message::BuildFinished(finished) => {
if !finished.success {
bail!(
"cargo build finished with errors for target: {} [{}]",
self.main_target,
self.triple
);
}
}
_ => {}
}
}
self.print_linker_warnings(&output_location);
let workspace_rustc_args = self.load_rustc_argset()?;
let exe = output_location.context("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information.")?;
if matches!(ctx.mode, BuildMode::Fat) {
self.run_fat_link(ctx, &exe, &workspace_rustc_args).await?;
}
ctx.status_start_bundle();
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,
})
}
fn bust_fingerprint(&self, ctx: &BuildContext) -> Result<()> {
if matches!(ctx.mode, BuildMode::Thin { .. }) {
return Ok(());
}
_ = std::fs::create_dir_all(&self.rustc_wrapper_args_scope_dir(&ctx.mode)?)
.context("Failed to create rustc wrapper args scope dir");
if !matches!(ctx.mode, BuildMode::Fat) {
return Ok(());
}
let mut bust = HashSet::new();
bust.insert(self.package().name.clone());
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(())
}
async fn write_executable(
&self,
ctx: &BuildContext,
artifacts: &mut BuildArtifacts,
) -> Result<()> {
match self.bundle {
BundleFormat::Web => {
self.bundle_web(ctx, &artifacts.exe, &mut artifacts.assets)
.await?;
}
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(())
}
async fn write_frameworks(&self, artifacts: &BuildArtifacts) -> Result<()> {
let framework_dir = self.frameworks_folder();
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 {
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);
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)?;
}
}
if self.bundle == BundleFormat::Android {
_ = std::fs::create_dir_all(&framework_dir);
}
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")?;
}
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(())
}
pub async fn collect_assets_and_metadata(
&self,
exe: &Path,
ctx: &BuildContext,
) -> Result<AppManifest> {
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)
}
async fn write_assets(&self, ctx: &BuildContext, assets: &AppManifest) -> Result<()> {
if !ctx.is_primary_build() {
return Ok(());
}
let asset_dir = self.bundle_asset_dir();
_ = std::fs::create_dir_all(&asset_dir);
let mut keep_bundled_output_paths: HashSet<_> = assets
.unique_assets()
.map(|a| asset_dir.join(a.bundled_path()))
.collect();
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
);
let mut assets_to_transfer = vec![];
for bundled in assets.unique_assets() {
let from = PathBuf::from(bundled.absolute_source_path());
let to = asset_dir.join(bundled.bundled_path());
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);
let progress = ctx.clone();
let ws_dir = self.workspace_dir();
let esbuild_path = crate::esbuild::Esbuild::path_if_installed();
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}"))??;
if self.should_bundle_to_asset() {
_ = std::fs::remove_dir_all(self.wasm_bindgen_out_dir());
}
self.write_app_manifest(assets).await?;
Ok(())
}
fn cargo_build_command(&self, build_mode: &BuildMode) -> Result<Command> {
match build_mode {
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)
}
_ => {
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)));
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")?,
);
cmd.env("RUSTC_WORKSPACE_WRAPPER", Workspace::path_to_dx()?);
Ok(cmd)
}
}
}
#[allow(clippy::vec_init_then_push)]
pub(crate) fn cargo_build_arguments(&self, build_mode: &BuildMode) -> Vec<String> {
let mut cargo_args = Vec::with_capacity(4);
cargo_args.extend(self.profile_args());
cargo_args.push("--profile".to_string());
cargo_args.push(self.profile.to_string());
cargo_args.push("--target".to_string());
cargo_args.push(self.triple.to_string());
cargo_args.push("--verbose".to_string());
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(" "));
}
cargo_args.push(String::from("-p"));
cargo_args.push(self.package.clone());
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());
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());
}
cargo_args.extend(self.extra_cargo_args.clone());
cargo_args.push("--".to_string());
cargo_args.extend(self.extra_rustc_args.clone());
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));
cargo_args.push("-Clink-arg=/ENTRY:mainCRTStartup".to_string());
}
if self.bundle == BundleFormat::Web && self.wasm_split {
cargo_args.push("-Clink-args=--emit-relocs".to_string());
}
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()
));
}
if self.bundle == BundleFormat::Android {
cargo_args.push("-Clink-arg=-Wl,--build-id=sha1".to_string());
}
match self.triple.operating_system {
OperatingSystem::Darwin(_) | OperatingSystem::MacOSX { .. } => {
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(_) => {
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]);
}
}
_ => {}
}
if matches!(build_mode, BuildMode::Thin { .. } | BuildMode::Fat) {
cargo_args.extend_from_slice(&[
"-Csave-temps=true".to_string(),
"-Clink-dead-code".to_string(),
]);
if matches!(
self.triple.architecture,
target_lexicon::Architecture::Wasm32 | target_lexicon::Architecture::Wasm64
) || self.triple.operating_system == OperatingSystem::Wasi
{
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<Vec<(Cow<'static, str>, OsString)>> {
let mut env_vars = vec![];
if self.bundle == BundleFormat::Android {
env_vars.extend(self.android_env_vars()?);
};
env_vars.push((PRODUCT_NAME_ENV.into(), self.bundled_app_name().into()));
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(),
));
}
let rust_flags = self.rustflags.clone();
if !rust_flags.flags.is_empty() {
env_vars.push((
"RUSTFLAGS".into(),
rust_flags
.encode_space_separated()
.context("Failed to encode RUSTFLAGS")?
.into(),
));
}
let use_dx_linker = self.custom_linker.is_some()
|| matches!(build_mode, BuildMode::Thin { .. } | BuildMode::Fat);
if use_dx_linker {
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)
}
async fn post_process_executable(&self, artifacts: &BuildArtifacts) -> Result<()> {
if self.wasm_split {
return Ok(());
}
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();
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(())
}
async fn write_metadata(&self) -> Result<()> {
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)?;
}
BundleFormat::Android => {}
BundleFormat::Windows => {}
BundleFormat::Linux => {}
BundleFormat::Web => {}
BundleFormat::Server => {}
}
Ok(())
}
async fn write_ffi_plugins(
&self,
ctx: &BuildContext,
artifacts: &BuildArtifacts,
) -> Result<()> {
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(", ")));
self.compile_swift_sources(&artifacts.assets.swift_sources)
.await
.context("Failed to compile Swift packages")?;
self.embed_swift_stdlibs(&artifacts.assets.swift_sources)
.await
.context("Failed to embed Swift standard libraries")?;
}
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(())
}
async fn optimize(&self, ctx: &BuildContext) -> Result<()> {
ctx.profile_phase("Optimizing Bundle");
match self.bundle {
BundleFormat::Web => {
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(())
}
async fn assemble(&self, ctx: &BuildContext) -> Result<()> {
ctx.profile_phase("Assembling Bundle");
if let BundleFormat::Android = self.bundle {
self.assemble_android(ctx).await?;
}
if self.is_apple_target() && self.should_codesign {
self.codesign_apple(ctx).await?;
}
Ok(())
}
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<Result<()>> = OnceLock::new();
static SECONDARY_INITIALIZED: OnceLock<Result<()>> = 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(),
);
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<AppManifest> {
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)
}
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();
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,
}
}
}
}
}
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<()> {
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(())
}
#[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() {
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(())
}
async fn verify_linux_tooling(&self) -> Result<()> {
Ok(())
}
async fn get_unit_count_estimate(&self, build_mode: &BuildMode) -> usize {
if let Ok(count) = self.get_unit_count(build_mode).await {
return count;
}
let units = self
.workspace
.krates
.krates_filtered(krates::DepKind::Dev)
.iter()
.map(|k| k.targets.len())
.sum::<usize>();
(units as f64 / 3.5) as usize
}
async fn get_unit_count(&self, build_mode: &BuildMode) -> crate::Result<usize> {
#[derive(Debug, Deserialize)]
struct UnitGraph {
units: Vec<serde_json::Value>,
}
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<PathBuf>) {
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);
}
}
}
}
fn load_rustc_argset(&self) -> Result<WorkspaceRustcArgs> {
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);
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::<RustcArgs>(&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<String> {
let mut features = self.features.clone();
features.dedup();
features
}
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;
};
fn get_strip(profile: &Profile, profiles: &Profiles) -> Option<StripSetting> {
profile.strip.as_ref().copied().or_else(|| {
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 {
return StripSetting::None;
};
strip
}
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(),
BundleFormat::MacOS => platform_dir.join(format!("{}.app", self.bundled_app_name())),
BundleFormat::Ios => platform_dir.join(format!("{}.app", self.bundled_app_name())),
BundleFormat::Android => platform_dir.join("app"), BundleFormat::Linux => platform_dir.join("app"), BundleFormat::Windows => platform_dir.join("app"), }
}
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 {
BundleFormat::MacOS | BundleFormat::Ios => self.executable_name().to_string(),
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(),
},
BundleFormat::Android => {
let lib_name = self.android_lib_name();
format!("lib{lib_name}.so")
}
BundleFormat::Web => format!("{}_bg.wasm", self.executable_name()),
BundleFormat::Linux => self.executable_name().to_string(),
}
}
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<PathBuf> {
Ok(self
.rustc_wrapper_args_dir()
.join(self.rustc_wrapper_scope_dir_name(build_mode)?))
}
pub(crate) fn tip_crate_name(&self) -> String {
self.main_target.replace('-', "_")
}
fn link_err_file(&self) -> PathBuf {
self.session_cache_dir().join("link_err.txt")
}
fn link_args_file(&self) -> PathBuf {
self.session_cache_dir().join("link_args.json")
}
pub(crate) fn windows_command_file(&self) -> PathBuf {
self.session_cache_dir().join("windows_command.txt")
}
pub(crate) fn crate_out_dir(&self) -> Option<PathBuf> {
self.config
.application
.out_dir
.as_ref()
.map(|out_dir| self.crate_dir().join(out_dir))
}
fn internal_out_dir(&self) -> PathBuf {
let dir = self.target_dir.join("dx");
std::fs::create_dir_all(&dir).unwrap();
dir
}
pub(crate) fn bundle_dir(&self, bundle: BundleFormat) -> PathBuf {
self.internal_out_dir()
.join(&self.main_target)
.join("bundle")
.join(bundle.build_folder_name())
}
pub(crate) fn workspace_dir(&self) -> PathBuf {
self.workspace
.krates
.workspace_root()
.as_std_path()
.to_path_buf()
}
pub(crate) fn crate_dir(&self) -> PathBuf {
self.package()
.manifest_path
.parent()
.unwrap()
.as_std_path()
.to_path_buf()
}
pub(crate) fn package(&self) -> &krates::cm::Package {
&self.workspace.krates[self.crate_package]
}
pub(crate) fn executable_name(&self) -> &str {
&self.crate_target.name
}
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)
}
pub(crate) fn crate_version(&self) -> String {
self.workspace.krates[self.crate_package]
.version
.to_string()
}
pub(crate) fn bundle_identifier(&self) -> String {
use crate::config::BundlePlatform;
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())
}
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
}
pub(crate) fn fullstack_feature_enabled(&self) -> bool {
let dioxus_dep = self
.package()
.dependencies
.iter()
.find(|dep| dep.name == "dioxus");
let Some(dioxus_dep) = dioxus_dep else {
return false;
};
if dioxus_dep.features.iter().any(|f| f == "fullstack") {
return true;
}
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"),
BundleFormat::Server => self.root_dir().join("public").join("assets"),
BundleFormat::Web | BundleFormat::Ios | BundleFormat::Windows | BundleFormat::Linux => {
self.root_dir().join("assets")
}
}
}
fn exe_dir(&self) -> PathBuf {
match self.bundle {
BundleFormat::MacOS => self.root_dir().join("Contents").join("MacOS"),
BundleFormat::Web => self.root_dir().join("wasm"),
BundleFormat::Android => self
.root_dir()
.join("app")
.join("src")
.join("main")
.join("jniLibs")
.join(AndroidTools::android_jnilib(&self.triple)),
BundleFormat::Windows
| BundleFormat::Linux
| BundleFormat::Ios
| BundleFormat::Server => self.root_dir(),
}
}
fn bundle_manifest_file(&self) -> PathBuf {
self.platform_dir().join(".manifest.json")
}
pub(crate) fn workspace_dependents_of(&self, crate_name: &str) -> Vec<String> {
let krates = &self.workspace.krates;
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();
};
let workspace_names: HashSet<String> = 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()
}
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(),
}
}
pub(crate) fn user_public_dir(&self) -> Option<PathBuf> {
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)
})
}
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))
}
pub(crate) fn trimmed_base_path(&self) -> Option<&str> {
self.base_path()
.map(|p| p.trim_matches('/'))
.filter(|p| !p.is_empty())
}
pub(crate) fn base_path_or_default(&self) -> &str {
self.trimmed_base_path().unwrap_or(".")
}
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 {
BundleFormat::Ios => self.start_ios_sim().await?,
BundleFormat::Android => self.start_android_sim()?,
_ => {}
};
Ok(())
}
fn profile_args(&self) -> Vec<String> {
let profile = self.profile.as_str();
let mut args = Vec::new();
args.push(format!(r#"profile.{profile}.strip=false"#));
if !self
.workspace
.cargo_toml
.profile
.custom
.contains_key(&self.profile)
{
let inherits = if self.release { "release" } else { "dev" };
args.push(format!(r#"profile.{profile}.inherits="{inherits}""#));
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"#));
}
}
}
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(_)
)
}
fn workspace_crate_dep_names(&self) -> Vec<String> {
let krates = &self.workspace.krates;
let workspace_names: HashSet<String> = krates
.workspace_members()
.filter_map(|m| match m {
krates::Node::Krate { krate, .. } => Some(krate.name.replace('-', "_")),
_ => None,
})
.collect();
let tip = self.tip_crate_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();
};
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
}
}