fix: workspace hotpatch cache thrashing, feat: add compile profiling, cleanup + refactor + document cli request.rs file (#5479)
* it works! * fix wasm hotpatch error * bump wasmopt * rename wasmopt flag * rename ldd_keep_exports * add build profiling * undo bundle ignore * some cleanups * more cleanups * wip: hoist up telemetry to builder * fix compile * wip: flamegraph rewrite * clean up flamegraph impl * cleanup profiling engine * add comment on spans obj * cleanup more phases * more cleanup * small cleanups * bit more cleanups * cleanup phases * sign with tokio, not std proc * split out the specific platform helpers * cleanup a bit * more cleanups * more cleanups * more cleanups * more cleanups * pull out fingerprint logic * wip... I think I have a slightly better approach. saving checkpoint * hm I think it's better now? * fix: also seems to work not in workspace mode juuust yet * okay yay almost there * clooose but not there yet * fix: manually export __wasm_apply_data_relocs since got relocations weren't firing WASM workspace hotpatching: export __wasm_apply_global_relocs to fix call_indirect type mismatch When workspace crates are recompiled with -Crelocation-model=pic for hotpatching, wasm-ld generates GOT.func.internal globals that hold element-segment-relative offsets (e.g., MetricCardProps::memoize = 31). These need __table_base added at runtime by __wasm_apply_global_relocs to become absolute table indices. wasm-ld generates __wasm_apply_global_relocs but treats it as an internal synthetic function -- neither --export nor --export-if-defined will export it. Without the export, the runtime's call silently failed (the old code used unchecked_into on undefined), leaving the globals unrelocated. Function pointers then pointed to index 31 in the original module's table (some unrelated function) instead of table_base + 31 (the patched function), causing call_indirect to a signature that does not match. Three changes: patch.rs: After walrus processing, find __wasm_apply_global_relocs by name and add it as an export before emitting the patched wasm. lib.rs: Replace unchecked_into with dyn_into for calling relocation functions, so missing exports are handled gracefully instead of silently swallowed. link.rs: Remove the ineffective --export-if-defined flags that were added during debugging. * cleanup * fix scope dir * change up naming a bit * more tidying up * more cleanups * ignore early return * clean er up * more comprehensive cache bustign * bust fingerprints of dag * move bust_fingerprint back to request.rs * add better rustdoc * more idiomatic * more cleanups * fix panic, typos * more lints * only delete workspace prints in fat mod * clippy
This commit is contained in:
1 parent
348208d1cb
commit
fb855b7088
37 files changed
+7276
-6865
No files matched your search
+19
-23
@@ -6,9 +6,25 @@ fn main() {
|
||||
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=.git/refs");
|
||||
|
||||
let full_hash = std::env::var("DIOXUS_CLI_GIT_SHA")
|
||||
.ok()
|
||||
.or_else(resolve_git_commit_hash);
|
||||
let full_hash = std::env::var("DIOXUS_CLI_GIT_SHA").ok().or_else(|| {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let hash = String::from_utf8(output.stdout).ok()?;
|
||||
let hash = hash.trim().to_string();
|
||||
|
||||
if hash.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(hash)
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(full_hash) = full_hash {
|
||||
println!("cargo:rustc-env=DIOXUS_CLI_GIT_SHA={full_hash}");
|
||||
@@ -20,23 +36,3 @@ fn main() {
|
||||
println!("cargo:rustc-env=DIOXUS_CLI_GIT_SHA_SHORT={short_hash}");
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_git_commit_hash() -> Option<String> {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let hash = String::from_utf8(output.stdout).ok()?;
|
||||
let hash = hash.trim().to_string();
|
||||
|
||||
if hash.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(hash)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -29,37 +29,27 @@
|
||||
//! build system.
|
||||
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
io::{Cursor, Read, Seek, Write},
|
||||
path::{Path, PathBuf},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::opt::AssetManifest;
|
||||
use crate::opt::AppManifest;
|
||||
use crate::Result;
|
||||
use anyhow::{bail, Context};
|
||||
use const_serialize::{deserialize_const, serialize_const, ConstVec};
|
||||
use manganis::{AssetOptions, AssetVariant, BundledAsset, ImageFormat, ImageSize};
|
||||
use manganis_core::{AndroidArtifactMetadata, SwiftPackageMetadata, SymbolData};
|
||||
use manganis_core::SymbolData;
|
||||
use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol};
|
||||
use pdb::FallibleIterator;
|
||||
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
||||
|
||||
/// Extract all manganis symbols and their sections from the given object file.
|
||||
fn manganis_symbols<'a, 'b, R: ReadRef<'a>>(
|
||||
file: &'b File<'a, R>,
|
||||
) -> impl Iterator<Item = (ManganisVersion, Symbol<'a, 'b, R>, Section<'a, 'b, R>)> + 'b {
|
||||
file.symbols().filter_map(move |symbol| {
|
||||
let name = symbol.name().ok()?;
|
||||
let version = looks_like_manganis_symbol(name)?;
|
||||
let section_index = symbol.section_index()?;
|
||||
let section = file.section_by_index(section_index).ok()?;
|
||||
Some((version, symbol, section))
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum ManganisVersion {
|
||||
/// The legacy version of the manganis format published with 0.7.0 and 0.7.1
|
||||
Legacy,
|
||||
|
||||
/// The new version of the manganis format 0.7.2 onward
|
||||
/// This now includes both assets (old BundledAsset format) and permissions (SymbolData format)
|
||||
New,
|
||||
@@ -696,37 +686,25 @@ fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>(
|
||||
Ok(offsets)
|
||||
}
|
||||
|
||||
/// Result of extracting symbols from a binary file
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SymbolExtractionResult {
|
||||
/// Assets found in the binary
|
||||
pub assets: Vec<BundledAsset>,
|
||||
|
||||
/// Android plugin artifacts discovered in the binary
|
||||
pub android_artifacts: Vec<AndroidArtifactMetadata>,
|
||||
|
||||
/// Swift packages discovered in the binary
|
||||
pub swift_packages: Vec<SwiftPackageMetadata>,
|
||||
}
|
||||
|
||||
/// Find all assets in the given file, hash them, and write them back to the file.
|
||||
/// Also extracts Android/Swift plugin metadata for FFI bindings.
|
||||
pub(crate) async fn extract_symbols_from_file(
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<SymbolExtractionResult> {
|
||||
pub(crate) async fn extract_symbols_from_file(path: impl AsRef<Path>) -> Result<AppManifest> {
|
||||
let path = path.as_ref();
|
||||
let mut file = open_file_for_writing_with_timeout(
|
||||
path,
|
||||
std::fs::OpenOptions::new().write(true).read(true),
|
||||
)
|
||||
.await?;
|
||||
let mut file =
|
||||
open_file_for_writing_with_timeout(path, OpenOptions::new().write(true).read(true)).await?;
|
||||
|
||||
let mut file_contents = Vec::new();
|
||||
file.read_to_end(&mut file_contents)?;
|
||||
let mut reader = Cursor::new(&file_contents);
|
||||
let read_cache = ReadCache::new(&mut reader);
|
||||
let object_file = object::File::parse(&read_cache)?;
|
||||
let offsets = find_symbol_offsets(path, &file_contents, &object_file)?;
|
||||
|
||||
let (offsets, obj_format) = {
|
||||
let mut reader = Cursor::new(&file_contents);
|
||||
let read_cache = ReadCache::new(&mut reader);
|
||||
let object_file = object::File::parse(&read_cache)?;
|
||||
(
|
||||
find_symbol_offsets(path, &file_contents, &object_file)?,
|
||||
object_file.format(),
|
||||
)
|
||||
};
|
||||
|
||||
let mut assets = Vec::new();
|
||||
let mut android_artifacts = Vec::new();
|
||||
@@ -824,7 +802,11 @@ pub(crate) async fn extract_symbols_from_file(
|
||||
|
||||
match entry.representation {
|
||||
AssetRepresentation::RawBundled => {
|
||||
tracing::debug!("Writing asset to offset {offset}: {:?}", asset);
|
||||
tracing::debug!(
|
||||
"Writing asset to offset {offset}: {:?} - {:?}",
|
||||
asset.absolute_source_path(),
|
||||
asset.bundled_path()
|
||||
);
|
||||
let new_data = version.serialize_asset(&asset);
|
||||
if new_data.len() > version.size() {
|
||||
tracing::warn!(
|
||||
@@ -861,14 +843,15 @@ pub(crate) async fn extract_symbols_from_file(
|
||||
.context("Failed to sync file after writing assets")?;
|
||||
|
||||
// If the file is a macos binary, we need to re-sign the modified binary
|
||||
if object_file.format() == object::BinaryFormat::MachO && !assets.is_empty() {
|
||||
if obj_format == object::BinaryFormat::MachO && !assets.is_empty() {
|
||||
// Spawn the codesign command to re-sign the binary
|
||||
let output = std::process::Command::new("codesign")
|
||||
let output = tokio::process::Command::new("codesign")
|
||||
.arg("--force")
|
||||
.arg("--sign")
|
||||
.arg("-") // Sign with an empty identity
|
||||
.arg(path)
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to run codesign - is `codesign` in your path?")?;
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
@@ -878,23 +861,15 @@ pub(crate) async fn extract_symbols_from_file(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SymbolExtractionResult {
|
||||
assets,
|
||||
android_artifacts,
|
||||
swift_packages,
|
||||
})
|
||||
}
|
||||
let mut manifest = AppManifest::new();
|
||||
|
||||
/// Find all assets in the given file, hash them, and write them back to the file.
|
||||
/// Then return an `AssetManifest` containing all the assets found in the file.
|
||||
///
|
||||
/// This is a convenience function that extracts symbols and returns only assets.
|
||||
pub(crate) async fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<AssetManifest> {
|
||||
let result = extract_symbols_from_file(path).await?;
|
||||
let mut manifest = AssetManifest::default();
|
||||
for asset in result.assets {
|
||||
for asset in assets {
|
||||
manifest.insert_asset(asset);
|
||||
}
|
||||
|
||||
manifest.android_artifacts = android_artifacts;
|
||||
manifest.swift_sources = swift_packages;
|
||||
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
@@ -903,10 +878,10 @@ pub(crate) async fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<A
|
||||
/// This is useful on windows where antivirus software might grab the executable before we have a chance to read it.
|
||||
async fn open_file_for_writing_with_timeout(
|
||||
file: &Path,
|
||||
options: &mut std::fs::OpenOptions,
|
||||
options: &mut OpenOptions,
|
||||
) -> Result<std::fs::File> {
|
||||
let start_time = std::time::Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(5);
|
||||
let start_time = Instant::now();
|
||||
let timeout = Duration::from_secs(5);
|
||||
loop {
|
||||
match options.open(file) {
|
||||
Ok(file) => return Ok(file),
|
||||
@@ -916,7 +891,7 @@ async fn open_file_for_writing_with_timeout(
|
||||
tracing::trace!(
|
||||
"Failed to open file because another process is using it. Retrying..."
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
@@ -946,3 +921,16 @@ fn write_serialized_bytes(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract all manganis symbols and their sections from the given object file.
|
||||
fn manganis_symbols<'a, 'b, R: ReadRef<'a>>(
|
||||
file: &'b File<'a, R>,
|
||||
) -> impl Iterator<Item = (ManganisVersion, Symbol<'a, 'b, R>, Section<'a, 'b, R>)> + 'b {
|
||||
file.symbols().filter_map(move |symbol| {
|
||||
let name = symbol.name().ok()?;
|
||||
let version = looks_like_manganis_symbol(name)?;
|
||||
let section_index = symbol.section_index()?;
|
||||
let section = file.section_by_index(section_index).ok()?;
|
||||
Some((version, symbol, section))
|
||||
})
|
||||
}
|
||||
+469
-130
@@ -1,8 +1,7 @@
|
||||
use crate::opt::process_file_to;
|
||||
use crate::{opt::process_file_to, BuildPhaseProfile};
|
||||
use crate::{
|
||||
build::cache::ObjectCache, serve::WebServer, verbosity_or_default, BuildArtifacts,
|
||||
BuildRequest, BuildStage, BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result,
|
||||
RustcArgs, StructuredOutput,
|
||||
serve::WebServer, verbosity_or_default, BuildArtifacts, BuildRequest, BuildStage,
|
||||
BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result, StructuredOutput,
|
||||
};
|
||||
use anyhow::{bail, Context, Error};
|
||||
use futures_util::{future::OptionFuture, pin_mut, FutureExt};
|
||||
@@ -10,7 +9,7 @@ use itertools::Itertools;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
env,
|
||||
time::{Duration, Instant, SystemTime},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
@@ -98,10 +97,10 @@ pub(crate) struct AppBuilder {
|
||||
pub compiled_crates: usize,
|
||||
pub expected_crates: usize,
|
||||
pub bundling_progress: f64,
|
||||
pub compile_start: Option<Instant>,
|
||||
pub compile_end: Option<Instant>,
|
||||
pub bundle_start: Option<Instant>,
|
||||
pub bundle_end: Option<Instant>,
|
||||
pub compile_start: Option<SystemTime>,
|
||||
pub compile_end: Option<SystemTime>,
|
||||
pub bundle_start: Option<SystemTime>,
|
||||
pub bundle_end: Option<SystemTime>,
|
||||
|
||||
/// The debugger for the app - must be enabled with the `d` key
|
||||
pub(crate) pid: Option<u32>,
|
||||
@@ -110,8 +109,8 @@ pub(crate) struct AppBuilder {
|
||||
/// Each patch includes objects from ALL crates in this set.
|
||||
pub modified_crates: HashSet<String>,
|
||||
|
||||
/// Cache of the latest `.rcgu.o` files for each modified workspace crate.
|
||||
pub object_cache: ObjectCache,
|
||||
/// The build profiling spans for us to generate a flamegraph from.
|
||||
pub profile_spans: Vec<BuildPhaseProfile>,
|
||||
}
|
||||
|
||||
impl AppBuilder {
|
||||
@@ -153,7 +152,7 @@ impl AppBuilder {
|
||||
expected_crates: 1,
|
||||
bundling_progress: 0.0,
|
||||
builds_opened: 0,
|
||||
compile_start: Some(Instant::now()),
|
||||
compile_start: Some(SystemTime::now()),
|
||||
aslr_reference: None,
|
||||
compile_end: None,
|
||||
bundle_start: None,
|
||||
@@ -169,7 +168,7 @@ impl AppBuilder {
|
||||
patch_cache: None,
|
||||
pid: None,
|
||||
modified_crates: HashSet::new(),
|
||||
object_cache: ObjectCache::new(&request.session_cache_dir()),
|
||||
profile_spans: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -181,19 +180,13 @@ impl AppBuilder {
|
||||
}
|
||||
|
||||
pub(crate) fn start(&mut self, mode: BuildMode, build_id: BuildId) {
|
||||
self.build_task = tokio::spawn({
|
||||
let request = self.build.clone();
|
||||
let tx = self.tx.clone();
|
||||
async move {
|
||||
let ctx = BuildContext {
|
||||
mode,
|
||||
build_id,
|
||||
tx: tx.clone(),
|
||||
};
|
||||
request.verify_tooling(&ctx).await?;
|
||||
request.prebuild(&ctx).await?;
|
||||
request.build(&ctx).await
|
||||
}
|
||||
let request = self.build.clone();
|
||||
let tx = self.tx.clone();
|
||||
self.build_task = tokio::spawn(async move {
|
||||
let ctx = BuildContext::new(tx, mode, build_id);
|
||||
request.verify_tooling(&ctx).await?;
|
||||
request.prebuild(&ctx).await?;
|
||||
request.build(ctx).await
|
||||
});
|
||||
}
|
||||
|
||||
@@ -260,69 +253,88 @@ impl AppBuilder {
|
||||
// doing so will cause the changes to be lost since this wait call is called under a cancellable task
|
||||
// todo - move this handling to a separate function that won't be cancelled
|
||||
match &update {
|
||||
BuilderUpdate::Progress { stage } => {
|
||||
BuilderUpdate::Progress { .. } if self.is_finished() => {
|
||||
// Prevent updates from flowing in after the build has already finished
|
||||
if !self.is_finished() {
|
||||
self.stage = stage.clone();
|
||||
}
|
||||
BuilderUpdate::Progress { stage } => {
|
||||
self.stage = stage.clone();
|
||||
|
||||
match stage {
|
||||
BuildStage::Initializing => {
|
||||
self.compiled_crates = 0;
|
||||
self.bundling_progress = 0.0;
|
||||
}
|
||||
BuildStage::Starting { crate_count, .. } => {
|
||||
self.expected_crates = *crate_count.max(&1);
|
||||
}
|
||||
BuildStage::InstallingTooling => {}
|
||||
BuildStage::Compiling { current, total, .. } => {
|
||||
self.compiled_crates = *current;
|
||||
self.expected_crates = *total.max(&1);
|
||||
|
||||
if self.compile_start.is_none() {
|
||||
self.compile_start = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
BuildStage::Bundling => {
|
||||
self.complete_compile();
|
||||
self.bundling_progress = 0.0;
|
||||
self.bundle_start = Some(Instant::now());
|
||||
}
|
||||
BuildStage::OptimizingWasm => {}
|
||||
BuildStage::CopyingAssets { current, total, .. } => {
|
||||
self.bundling_progress = *current as f64 / *total as f64;
|
||||
}
|
||||
BuildStage::Success => {
|
||||
self.compiled_crates = self.expected_crates;
|
||||
self.bundling_progress = 1.0;
|
||||
}
|
||||
BuildStage::Failed => {
|
||||
self.compiled_crates = self.expected_crates;
|
||||
self.bundling_progress = 1.0;
|
||||
}
|
||||
BuildStage::Aborted => {}
|
||||
BuildStage::Restarting => {
|
||||
self.compiled_crates = 0;
|
||||
self.expected_crates = 1;
|
||||
self.bundling_progress = 0.0;
|
||||
}
|
||||
BuildStage::RunningBindgen => {}
|
||||
_ => {}
|
||||
match stage {
|
||||
BuildStage::Initializing => {
|
||||
self.profile_spans.clear();
|
||||
self.compiled_crates = 0;
|
||||
self.bundling_progress = 0.0;
|
||||
}
|
||||
BuildStage::Starting { crate_count, .. } => {
|
||||
self.expected_crates = *crate_count.max(&1);
|
||||
}
|
||||
BuildStage::InstallingTooling => {}
|
||||
BuildStage::Compiling { current, total, .. } => {
|
||||
self.compiled_crates = *current;
|
||||
self.expected_crates = *total.max(&1);
|
||||
|
||||
if self.compile_start.is_none() {
|
||||
self.compile_start = Some(SystemTime::now());
|
||||
}
|
||||
}
|
||||
BuildStage::Bundling => {
|
||||
self.bundling_progress = 0.0;
|
||||
self.bundle_start = Some(SystemTime::now());
|
||||
|
||||
if self.compile_end.is_none() {
|
||||
self.compiled_crates = self.expected_crates;
|
||||
self.compile_end = Some(SystemTime::now());
|
||||
}
|
||||
}
|
||||
BuildStage::OptimizingWasm => {}
|
||||
BuildStage::CopyingAssets { current, total, .. } => {
|
||||
self.bundling_progress = *current as f64 / *total as f64;
|
||||
}
|
||||
BuildStage::Success => {
|
||||
self.compiled_crates = self.expected_crates;
|
||||
self.bundling_progress = 1.0;
|
||||
}
|
||||
BuildStage::Failed => {
|
||||
self.compiled_crates = self.expected_crates;
|
||||
self.bundling_progress = 1.0;
|
||||
}
|
||||
BuildStage::Aborted => {}
|
||||
BuildStage::Restarting => {
|
||||
self.compiled_crates = 0;
|
||||
self.expected_crates = 1;
|
||||
self.bundling_progress = 0.0;
|
||||
}
|
||||
BuildStage::RunningBindgen => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
BuilderUpdate::CompilerMessage { .. } => {}
|
||||
BuilderUpdate::BuildReady { .. } => {
|
||||
BuilderUpdate::BuildReady { ref bundle } => {
|
||||
// Log the build completion as a telemetry event + provide analytics on build phases
|
||||
self.log_flamegraph_and_telemetry(bundle);
|
||||
|
||||
// And then update the build state
|
||||
self.compiled_crates = self.expected_crates;
|
||||
self.bundling_progress = 1.0;
|
||||
self.stage = BuildStage::Success;
|
||||
|
||||
self.complete_compile();
|
||||
self.bundle_end = Some(Instant::now());
|
||||
self.bundle_end = Some(SystemTime::now());
|
||||
if self.compile_end.is_none() {
|
||||
self.compiled_crates = self.expected_crates;
|
||||
self.compile_end = Some(SystemTime::now());
|
||||
}
|
||||
}
|
||||
BuilderUpdate::BuildFailed { .. } => {
|
||||
tracing::debug!("Setting builder to failed state");
|
||||
self.stage = BuildStage::Failed;
|
||||
}
|
||||
BuilderUpdate::ProfilePhase { profile } => {
|
||||
// Collapse consecutive entries with the same label so e.g. per-crate "Compiling"
|
||||
// calls show up as a single span instead of hundreds.
|
||||
if self.profile_spans.last().map(|p| p.label) != Some(profile.label) {
|
||||
self.profile_spans.push(profile.clone());
|
||||
}
|
||||
}
|
||||
BuilderUpdate::CompilerMessage { .. } => {}
|
||||
StdoutReceived { .. } => {}
|
||||
StderrReceived { .. } => {}
|
||||
ProcessExited { .. } => {}
|
||||
@@ -332,6 +344,23 @@ impl AppBuilder {
|
||||
update
|
||||
}
|
||||
|
||||
/// Kick off a thin (hotpatch) rebuild in response to a file change, replacing any in-flight
|
||||
/// build with one that produces a relinkable patch dylib (later applied by [`AppBuilder::hotpatch`]).
|
||||
///
|
||||
/// Bails early if there's no prior fat build to link against, or if we're missing the ASLR
|
||||
/// reference on a non-wasm target (which means no client is connected yet to report it).
|
||||
///
|
||||
/// Updates the cumulative `modified_crates` set: every patch is self-contained and contains
|
||||
/// the latest version of *every* crate modified since the fat build, not just the one that
|
||||
/// changed this iteration. We BFS `workspace_dependents_of` from `changed_crates` to catch
|
||||
/// the cascade — e.g. editing a leaf crate forces parent crates' generic instantiations to
|
||||
/// change too. (`compile_workspace_deps` does its own walk to decide what to *compile* now;
|
||||
/// this set tracks what to *link* into the patch.)
|
||||
///
|
||||
/// Then aborts any in-flight build and spawns a fresh task with [`BuildMode::Thin`], handing
|
||||
/// it the fat build's `workspace_rustc_args`/`artifact_paths` (so rustc gets re-invoked with
|
||||
/// identical flags), the `HotpatchModuleCache` (symbol map for `create_jump_table`), and the
|
||||
/// current `object_cache` (per-crate `.rcgu.o` files used for assembly diffing).
|
||||
pub(crate) fn patch_rebuild(
|
||||
&mut self,
|
||||
changed_files: Vec<PathBuf>,
|
||||
@@ -405,22 +434,22 @@ impl AppBuilder {
|
||||
|
||||
// Abort all the ongoing builds, cleaning up any loose artifacts and waiting to cleanly exit
|
||||
self.abort_all(BuildStage::Restarting);
|
||||
self.compile_start = Some(SystemTime::now());
|
||||
self.profile_spans.clear();
|
||||
self.build_task = tokio::spawn({
|
||||
let request = self.build.clone();
|
||||
let ctx = BuildContext {
|
||||
build_id,
|
||||
tx: self.tx.clone(),
|
||||
mode: BuildMode::Thin {
|
||||
let ctx = BuildContext::new(
|
||||
self.tx.clone(),
|
||||
BuildMode::Thin {
|
||||
changed_files,
|
||||
changed_crates,
|
||||
modified_crates: self.modified_crates.clone(),
|
||||
workspace_rustc_args: artifacts.workspace_rustc_args,
|
||||
workspace_rustc_args: artifacts.workspace_rustc,
|
||||
aslr_reference,
|
||||
cache,
|
||||
object_cache: self.object_cache.clone(),
|
||||
},
|
||||
};
|
||||
async move { request.build(&ctx).await }
|
||||
build_id,
|
||||
);
|
||||
async move { request.build(ctx).await }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -434,15 +463,14 @@ impl AppBuilder {
|
||||
|
||||
// A full rebuild resets all accumulated hotpatch state — the fat binary is a clean baseline.
|
||||
self.modified_crates.clear();
|
||||
self.object_cache = ObjectCache::new(&self.build.session_cache_dir());
|
||||
self.profile_spans.clear();
|
||||
self.build_task = tokio::spawn({
|
||||
let request = self.build.clone();
|
||||
let ctx = BuildContext {
|
||||
tx: self.tx.clone(),
|
||||
mode,
|
||||
build_id,
|
||||
};
|
||||
async move { request.build(&ctx).await }
|
||||
let tx = self.tx.clone();
|
||||
async move {
|
||||
let ctx = BuildContext::new(tx, mode, build_id);
|
||||
request.build(ctx).await
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -520,6 +548,7 @@ impl AppBuilder {
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
BuilderUpdate::ProfilePhase { .. } => {}
|
||||
BuilderUpdate::StdoutReceived { .. } => {}
|
||||
BuilderUpdate::StderrReceived { .. } => {}
|
||||
BuilderUpdate::ProcessExited { .. } => {}
|
||||
@@ -536,7 +565,7 @@ impl AppBuilder {
|
||||
///
|
||||
/// Note that Dioxus apps *should not* rely on this vars being set, but libraries like Bevy do.
|
||||
pub(crate) fn child_environment_variables(
|
||||
&mut self,
|
||||
&self,
|
||||
devserver_ip: Option<SocketAddr>,
|
||||
start_fullstack_on_address: Option<SocketAddr>,
|
||||
always_on_top: bool,
|
||||
@@ -605,22 +634,16 @@ impl AppBuilder {
|
||||
));
|
||||
}
|
||||
|
||||
// If there's any CARGO vars in the rustc_wrapper files, push those too.
|
||||
// Read from any per-crate args file in the directory (they all share the same CARGO_ envs).
|
||||
if let Ok(entries) = std::fs::read_dir(self.build.rustc_wrapper_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) {
|
||||
for (key, value) in args.envs {
|
||||
if key.starts_with("CARGO_") {
|
||||
envs.push((key, value));
|
||||
}
|
||||
}
|
||||
break; // Only need one file for CARGO_ env vars
|
||||
}
|
||||
}
|
||||
// If there's any CARGO vars in the captured rustc args, push those too.
|
||||
// Any captured invocation is sufficient since the CARGO_ environment is shared.
|
||||
if let Some(args) = self
|
||||
.artifacts
|
||||
.as_ref()
|
||||
.and_then(|artifacts| artifacts.workspace_rustc.rustc_args.values().next())
|
||||
{
|
||||
for (key, value) in args.envs.iter().cloned() {
|
||||
if key.starts_with("CARGO_") {
|
||||
envs.push((key, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -735,6 +758,56 @@ impl AppBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the artifacts produced by a thin (hotpatch) build to the currently running app and
|
||||
/// return a [`JumpTable`] that the runtime can use to swap function pointers in place.
|
||||
///
|
||||
/// This is the CLI-side counterpart to `subsecond`'s runtime patch loader. A thin build
|
||||
/// produces a self-contained dylib (`patch_exe`) that links against the symbols already
|
||||
/// resident in the running process. To make that dylib actually take effect, this function
|
||||
/// has to do four things, in order:
|
||||
///
|
||||
/// 1. **Reconcile new assets.** Walks `res.assets` and, for any `asset!()` reference that
|
||||
/// wasn't in the previous build, processes the source file (via `process_file_to`, which
|
||||
/// runs the asset through the appropriate optimizer — esbuild for JS/CSS, image
|
||||
/// re-encoding, etc.) and copies it into the live `asset_dir`. New assets are also
|
||||
/// inserted into `self.artifacts.assets` so subsequent patches see them as already-known.
|
||||
/// On Android the asset is additionally `adb push`'d to `/data/local/tmp/dx/assets/...`
|
||||
/// so the on-device app can read it.
|
||||
///
|
||||
/// 2. **Extend the file watcher set.** New `include!()` / `include_str!()` / `include_bytes!()`
|
||||
/// targets discovered in `res.depinfo.files` are appended to the existing artifacts'
|
||||
/// depinfo so the file watcher will pick up future edits to them.
|
||||
///
|
||||
/// 3. **Build the jump table.** Calls [`BuildRequest::create_jump_table`] with the new dylib
|
||||
/// and the [`HotpatchModuleCache`] (which holds the symbol map from the original fat
|
||||
/// build). The cache is what lets us resolve "function `foo` in the new dylib" to "the
|
||||
/// address of `foo` in the running process" so the runtime can patch the call sites. On
|
||||
/// Android the dylib itself is also pushed to the device and `jump_table.lib` is
|
||||
/// rewritten to point at the on-device path, since the runtime will `dlopen` it from
|
||||
/// there rather than from the host filesystem.
|
||||
///
|
||||
/// 4. **Commit local state.** The jump table is appended to `self.patches` (the cumulative
|
||||
/// history of patches applied to this run of the app — used so that a fresh client
|
||||
/// connecting mid-session can be brought up to date by replaying every patch in order),
|
||||
/// and `self.object_cache` is overwritten with `res.object_cache` so the next thin build
|
||||
/// diffs against the objects this patch produced rather than the previous tip.
|
||||
///
|
||||
/// The returned [`JumpTable`] is what gets shipped to the runtime over the devserver
|
||||
/// websocket; the runtime then `dlopen`s `jump_table.lib` and rewrites the trampolines
|
||||
/// described by the table.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if there are no prior `artifacts` to patch against (i.e. no fat build
|
||||
/// has completed yet), if `create_jump_table` fails (typically a symbol-resolution failure
|
||||
/// indicating the patch references something that doesn't exist in the base binary), or if
|
||||
/// the Android `adb push` of the dylib fails. Failures while copying individual assets are
|
||||
/// logged and skipped rather than aborting the patch.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `res.mode` is [`BuildMode::Thin`] but `changed_files` is empty — every thin
|
||||
/// build is triggered by at least one file change, so this should be unreachable in practice.
|
||||
pub(crate) async fn hotpatch(
|
||||
&mut self,
|
||||
res: &BuildArtifacts,
|
||||
@@ -742,7 +815,7 @@ impl AppBuilder {
|
||||
) -> Result<JumpTable> {
|
||||
let original = self.build.main_exe();
|
||||
let new = self.build.patch_exe(res.time_start);
|
||||
let asset_dir = self.build.asset_dir();
|
||||
let asset_dir = self.build.bundle_asset_dir();
|
||||
|
||||
// Hotpatch asset!() calls
|
||||
for bundled in res.assets.unique_assets() {
|
||||
@@ -822,9 +895,6 @@ impl AppBuilder {
|
||||
// Commit this patch
|
||||
self.patches.push(jump_table.clone());
|
||||
|
||||
// Sync the updated object cache back from the build artifacts.
|
||||
self.object_cache = res.object_cache.clone();
|
||||
|
||||
Ok(jump_table)
|
||||
}
|
||||
|
||||
@@ -850,7 +920,7 @@ impl AppBuilder {
|
||||
// we won't actually be using the build dir.
|
||||
let asset_dir = match self.runtime_asset_dir.as_ref() {
|
||||
Some(dir) => dir.to_path_buf().join("assets/"),
|
||||
None => self.build.asset_dir(),
|
||||
None => self.build.bundle_asset_dir(),
|
||||
};
|
||||
|
||||
// Canonicalize the path as Windows may use long-form paths "\\\\?\\C:\\".
|
||||
@@ -1558,32 +1628,23 @@ impl AppBuilder {
|
||||
main_exe
|
||||
}
|
||||
|
||||
fn complete_compile(&mut self) {
|
||||
if self.compile_end.is_none() {
|
||||
self.compiled_crates = self.expected_crates;
|
||||
self.compile_end = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the total duration of the build, if all stages have completed
|
||||
pub(crate) fn total_build_time(&self) -> Option<Duration> {
|
||||
Some(self.compile_duration()? + self.bundle_duration()?)
|
||||
}
|
||||
|
||||
pub(crate) fn compile_duration(&self) -> Option<Duration> {
|
||||
Some(
|
||||
self.compile_end
|
||||
.unwrap_or_else(Instant::now)
|
||||
.duration_since(self.compile_start?),
|
||||
)
|
||||
self.compile_end
|
||||
.unwrap_or_else(SystemTime::now)
|
||||
.duration_since(self.compile_start?)
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) fn bundle_duration(&self) -> Option<Duration> {
|
||||
Some(
|
||||
self.bundle_end
|
||||
.unwrap_or_else(Instant::now)
|
||||
.duration_since(self.bundle_start?),
|
||||
)
|
||||
self.bundle_end
|
||||
.unwrap_or_else(SystemTime::now)
|
||||
.duration_since(self.bundle_start?)
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Return a number between 0 and 1 representing the progress of the app build
|
||||
@@ -1868,4 +1929,282 @@ impl AppBuilder {
|
||||
|
||||
Ok(pid)
|
||||
}
|
||||
|
||||
/// Log the profile flamegraph for this build run, and then emit a telemetry event.
|
||||
///
|
||||
/// This works by walking the profile_spans vec, calculating the deltas, and then rendering out
|
||||
/// the flamegraph via a debug!()
|
||||
///
|
||||
/// The flamegraph looks something like this:
|
||||
///
|
||||
/// ```txt
|
||||
/// 17:22:56 [dev] Flamegraph for fat - time taken: 11371ms
|
||||
/// 0ms 0.0% Verify Tooling |█ |
|
||||
/// 0ms 0.0% Installing Tooling |█ |
|
||||
/// 100ms 0.9% Prebuild |██ |
|
||||
/// 307ms 2.7% Workspace precompile | ███ |
|
||||
/// 1400ms 12.3% Compiling | █████████████ |
|
||||
/// 1325ms 11.7% Fat Linking | ████████████ |
|
||||
/// 915ms 8.0% Extracting assets | █████████ |
|
||||
/// 1529ms 13.4% Writing executable | ██████████████ |
|
||||
/// 4685ms 41.2% Wasm Bindgen | █████████████████████████████████████████ |
|
||||
/// 1061ms 9.3% Creating Patch Cache | █████████|
|
||||
/// ```
|
||||
fn log_flamegraph_and_telemetry(&self, bundle: &BuildArtifacts) {
|
||||
let Some(start) = self.compile_start else {
|
||||
tracing::debug!("no compile start?");
|
||||
return;
|
||||
};
|
||||
|
||||
// Record the build duration as a telemetry event. Capture `now` once and use
|
||||
// it as the end of the final phase too, so the flamegraph bars stay
|
||||
// internally consistent with `time_taken`.
|
||||
let now = SystemTime::now();
|
||||
let time_taken = now
|
||||
.duration_since(start)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
.max(1);
|
||||
|
||||
tracing::debug!(
|
||||
telemetry = %serde_json::json!({
|
||||
"event": "build_and_bundle_complete",
|
||||
"time_taken": time_taken,
|
||||
"mode": match bundle.mode {
|
||||
BuildMode::Base { .. } => "base",
|
||||
BuildMode::Fat => "fat",
|
||||
BuildMode::Thin { .. } => "thin",
|
||||
},
|
||||
"blah": 123,
|
||||
"triple": self.build.triple.to_string(),
|
||||
"format": self.build.bundle.to_string(),
|
||||
"num_dependencies": self.build.workspace.krates.len(),
|
||||
}),
|
||||
"Build completed in {time_taken}ms",
|
||||
);
|
||||
|
||||
// Render the flamegraph out by walking the span history. This is pretty naive
|
||||
// and doesn't support nested span contexts (yet!) - all phases live on one row
|
||||
// and a phase ends where the next one begins.
|
||||
use std::fmt::Write as _;
|
||||
|
||||
let total_ms = time_taken as usize;
|
||||
let timeline_width = 96usize;
|
||||
let max_label_width = self
|
||||
.profile_spans
|
||||
.iter()
|
||||
.map(|phase| phase.label.len())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
.min(28);
|
||||
|
||||
// The final phase runs right up until `now`. Don't use `compile_end` here:
|
||||
// it's set when the Bundling stage starts, so it predates the bundling phases
|
||||
// themselves and would zero out the last bar.
|
||||
let phase_end = now;
|
||||
|
||||
let mut flamegraph = format!(
|
||||
"Flamegraph for {} - time taken: {}ms",
|
||||
match bundle.mode {
|
||||
BuildMode::Base { .. } => "base",
|
||||
BuildMode::Fat => "fat",
|
||||
BuildMode::Thin { .. } => "thin",
|
||||
},
|
||||
time_taken
|
||||
);
|
||||
|
||||
let mut phase_iter = self.profile_spans.iter().peekable();
|
||||
while let Some(phase) = phase_iter.next() {
|
||||
let end_time = phase_iter.peek().map(|f| f.start).unwrap_or(phase_end);
|
||||
|
||||
let offset_ms = phase
|
||||
.start
|
||||
.duration_since(start)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as usize;
|
||||
let dur_ms = end_time
|
||||
.duration_since(phase.start)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as usize;
|
||||
let pct = (dur_ms as f64) * 100.0 / (total_ms as f64);
|
||||
|
||||
let bar_start = ((offset_ms * timeline_width) / total_ms).min(timeline_width - 1);
|
||||
let bar_end = (((offset_ms + dur_ms) * timeline_width).div_ceil(total_ms))
|
||||
.max(bar_start + 1)
|
||||
.min(timeline_width);
|
||||
|
||||
let mut bar = vec![' '; timeline_width];
|
||||
for ch in &mut bar[bar_start..bar_end] {
|
||||
*ch = '█';
|
||||
}
|
||||
let bar: String = bar.into_iter().collect();
|
||||
|
||||
// Labels are ASCII so byte slicing is safe; truncate so the bar column
|
||||
// stays aligned across rows.
|
||||
let label = if phase.label.len() > max_label_width {
|
||||
&phase.label[..max_label_width]
|
||||
} else {
|
||||
phase.label
|
||||
};
|
||||
|
||||
let _ = write!(
|
||||
flamegraph,
|
||||
"\n {:>6}ms {:>5.1}% {:<width$} |{}|",
|
||||
dur_ms,
|
||||
pct,
|
||||
label,
|
||||
bar,
|
||||
width = max_label_width
|
||||
);
|
||||
}
|
||||
|
||||
tracing::debug!("{}", flamegraph);
|
||||
}
|
||||
|
||||
/// Pre-render the static routes, performing static-site generation
|
||||
pub(crate) async fn pre_render_static_routes(
|
||||
&mut self,
|
||||
devserver_ip: Option<SocketAddr>,
|
||||
updates: Option<&futures_channel::mpsc::UnboundedSender<BuilderUpdate>>,
|
||||
) -> anyhow::Result<()> {
|
||||
use super::{BuildId, BuilderUpdate};
|
||||
use anyhow::Context;
|
||||
use dioxus_cli_config::{server_ip, server_port};
|
||||
use dioxus_dx_wire_format::BuildStage;
|
||||
use futures_util::{stream::FuturesUnordered, StreamExt};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::process::Command;
|
||||
|
||||
if let Some(updates) = updates {
|
||||
updates
|
||||
.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::Prerendering,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
let server_exe = self.build.main_exe();
|
||||
|
||||
// Use the address passed in through environment variables or default to localhost:9999. We need
|
||||
// to default to a value that is different than the CLI default address to avoid conflicts
|
||||
let ip = server_ip().unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
||||
let port = server_port().unwrap_or(9999);
|
||||
let fullstack_address = SocketAddr::new(ip, port);
|
||||
let address = fullstack_address.ip().to_string();
|
||||
let port = fullstack_address.port().to_string();
|
||||
|
||||
// Borrow port and address so we can easily move them into multiple tasks below
|
||||
let address = &address;
|
||||
let port = &port;
|
||||
|
||||
tracing::info!("Running SSG at http://{address}:{port} for {server_exe:?}");
|
||||
|
||||
let vars = self.child_environment_variables(
|
||||
devserver_ip,
|
||||
Some(fullstack_address),
|
||||
false,
|
||||
BuildId::SECONDARY,
|
||||
);
|
||||
|
||||
// Run the server executable
|
||||
let _child = Command::new(&server_exe)
|
||||
.envs(vars)
|
||||
.current_dir(server_exe.parent().unwrap())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
// Borrow reqwest_client so we only move the reference into the futures
|
||||
let reqwest_client = reqwest::Client::new();
|
||||
let reqwest_client = &reqwest_client;
|
||||
|
||||
// Get the routes from the `/static_routes` endpoint
|
||||
let mut routes = None;
|
||||
|
||||
// The server may take a few seconds to start up. Try fetching the route up to 5 times with a one second delay
|
||||
const RETRY_ATTEMPTS: usize = 5;
|
||||
for i in 0..=RETRY_ATTEMPTS {
|
||||
tracing::debug!(
|
||||
"Attempting to get static routes from server. Attempt {i} of {RETRY_ATTEMPTS}"
|
||||
);
|
||||
|
||||
let request = reqwest_client
|
||||
.post(format!("http://{address}:{port}/api/static_routes"))
|
||||
.body("{}".to_string())
|
||||
.send()
|
||||
.await;
|
||||
match request {
|
||||
Ok(request) => {
|
||||
routes = Some(request
|
||||
.json::<Vec<String>>()
|
||||
.await
|
||||
.inspect(|text| tracing::debug!("Got static routes: {text:?}"))
|
||||
.context("Failed to parse static routes from the server. Make sure your server function returns Vec<String> with the (default) json encoding")?);
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
// If the request fails, try up to 5 times with a one second delay
|
||||
// If it fails 5 times, return the error
|
||||
if i == RETRY_ATTEMPTS {
|
||||
return Err(err).context("Failed to get static routes from server. Make sure you have a server function at the `/api/static_routes` endpoint that returns Vec<String> of static routes.");
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let routes = routes.expect(
|
||||
"static routes should exist or an error should have been returned on the last attempt",
|
||||
);
|
||||
|
||||
// Create a pool of futures that cache each route
|
||||
let mut resolved_routes = routes
|
||||
.into_iter()
|
||||
.map(|route| async move {
|
||||
tracing::info!("Rendering {route} for SSG");
|
||||
|
||||
// For each route, ping the server to force it to cache the response for ssg
|
||||
let request = reqwest_client
|
||||
.get(format!("http://{address}:{port}{route}"))
|
||||
.header("Accept", "text/html")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
// If it takes longer than 30 seconds to resolve the route, log a warning
|
||||
let warning_task = tokio::spawn({
|
||||
let route = route.clone();
|
||||
async move {
|
||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
tracing::warn!("Route {route} has been rendering for 30 seconds");
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the streaming response to completely finish before continuing. We don't use the html it returns directly
|
||||
// because it may contain artifacts of intermediate streaming steps while the page is loading. The SSG app should write
|
||||
// the final clean HTML to the disk automatically after the request completes.
|
||||
let _html = request.text().await?;
|
||||
|
||||
// Cancel the warning task if it hasn't already run
|
||||
warning_task.abort();
|
||||
|
||||
Ok::<_, reqwest::Error>(route)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
while let Some(route) = resolved_routes.next().await {
|
||||
match route {
|
||||
Ok(route) => tracing::debug!("ssg success: {route:?}"),
|
||||
Err(err) => tracing::error!("ssg error: {err:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("SSG complete");
|
||||
|
||||
drop(_child);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
//! Object file cache for workspace hotpatching.
|
||||
//!
|
||||
//! Maintains the latest `.rcgu.o` files for each crate in the cumulative `modified_crates` set.
|
||||
//! Used for accumulated relinking: combining objects from all modified crates into a single
|
||||
//! patch dylib.
|
||||
//!
|
||||
//! Objects are stored on disk under `session_cache_dir/object_cache/{crate_name}/` so they
|
||||
//! persist across patches without holding file contents in memory. The session cache dir
|
||||
//! lives in `/tmp/` and is cleaned up by the OS.
|
||||
//!
|
||||
//! - **Dep crates:** objects are extracted from their rlib in `target/deps/`.
|
||||
//! - **Tip crate:** objects are copied from linker arg paths since incremental compilation
|
||||
//! overwrites them in place.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Cache of compiled object files on disk, keyed by crate name.
|
||||
///
|
||||
/// After each compilation, the cache is updated for the compiled crate by extracting
|
||||
/// objects to `dir/{crate_name}/`. On relink, paths from all crates in `modified_crates`
|
||||
/// are passed directly to the linker — no intermediate copy needed.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ObjectCache {
|
||||
/// Root directory: `session_cache_dir/object_cache/`
|
||||
dir: PathBuf,
|
||||
|
||||
/// crate_name -> object file paths on disk
|
||||
objects: HashMap<String, Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
impl ObjectCache {
|
||||
pub fn new(session_cache_dir: &Path) -> Self {
|
||||
let dir = session_cache_dir.join("object_cache");
|
||||
Self {
|
||||
dir,
|
||||
objects: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract `.rcgu.o` files from an rlib archive and write them to
|
||||
/// `dir/{crate_name}/`. Replaces any previously cached objects for this crate.
|
||||
pub fn cache_from_rlib(&mut self, crate_name: &str, rlib_path: &Path) -> anyhow::Result<()> {
|
||||
let crate_dir = self.dir.join(crate_name);
|
||||
// Clear previous objects for this crate
|
||||
if crate_dir.exists() {
|
||||
std::fs::remove_dir_all(&crate_dir)?;
|
||||
}
|
||||
std::fs::create_dir_all(&crate_dir)?;
|
||||
|
||||
let rlib_contents = std::fs::read(rlib_path)?;
|
||||
let mut reader = ar::Archive::new(std::io::Cursor::new(rlib_contents));
|
||||
let mut paths = Vec::new();
|
||||
|
||||
while let Some(Ok(mut entry)) = reader.next_entry() {
|
||||
let name = std::str::from_utf8(entry.header().identifier())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
// Skip rmeta and empty entries
|
||||
if name.ends_with(".rmeta") || entry.header().size() == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only keep object files
|
||||
if !name.ends_with(".o") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut data = Vec::with_capacity(entry.header().size() as usize);
|
||||
entry.read_to_end(&mut data)?;
|
||||
|
||||
let obj_path = crate_dir.join(&name);
|
||||
std::fs::write(&obj_path, &data)?;
|
||||
paths.push(obj_path);
|
||||
}
|
||||
|
||||
self.objects.insert(crate_name.to_string(), paths);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cache tip crate objects by copying them from their filesystem paths.
|
||||
///
|
||||
/// Tip crate `.rcgu.o` files get overwritten on recompilation, so we copy
|
||||
/// them into our cache directory for stable access.
|
||||
pub fn cache_from_paths(
|
||||
&mut self,
|
||||
crate_name: &str,
|
||||
object_paths: &[impl AsRef<Path>],
|
||||
) -> anyhow::Result<()> {
|
||||
let crate_dir = self.dir.join(crate_name);
|
||||
if crate_dir.exists() {
|
||||
std::fs::remove_dir_all(&crate_dir)?;
|
||||
}
|
||||
std::fs::create_dir_all(&crate_dir)?;
|
||||
|
||||
let mut paths = Vec::with_capacity(object_paths.len());
|
||||
|
||||
for path in object_paths {
|
||||
let path = path.as_ref();
|
||||
let name = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or_default();
|
||||
let dest = crate_dir.join(name);
|
||||
std::fs::copy(path, &dest)?;
|
||||
paths.push(dest);
|
||||
}
|
||||
|
||||
self.objects.insert(crate_name.to_string(), paths);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get cached object file paths for a crate.
|
||||
pub fn get(&self, crate_name: &str) -> Option<&Vec<PathBuf>> {
|
||||
self.objects.get(crate_name)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use crate::{BuildArtifacts, BuildStage, Error, TraceSrc};
|
||||
use cargo_metadata::diagnostic::Diagnostic;
|
||||
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, process::ExitStatus};
|
||||
use std::{path::PathBuf, process::ExitStatus, time::SystemTime};
|
||||
|
||||
/// The context of the build process. While the BuildRequest is a "plan" for the build, the BuildContext
|
||||
/// provides some dynamic configuration that is only known at runtime. For example, the Progress channel
|
||||
@@ -35,6 +35,10 @@ pub enum BuilderUpdate {
|
||||
stage: BuildStage,
|
||||
},
|
||||
|
||||
ProfilePhase {
|
||||
profile: BuildPhaseProfile,
|
||||
},
|
||||
|
||||
CompilerMessage {
|
||||
message: Diagnostic,
|
||||
},
|
||||
@@ -78,20 +82,41 @@ pub enum BuilderUpdate {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BuildPhaseProfile {
|
||||
pub label: &'static str,
|
||||
pub start: SystemTime,
|
||||
}
|
||||
|
||||
impl BuildContext {
|
||||
pub(crate) fn new(tx: ProgressTx, mode: BuildMode, build_id: BuildId) -> Self {
|
||||
Self { tx, mode, build_id }
|
||||
}
|
||||
|
||||
/// Returns true if this is a client build - basically, is this the primary build?
|
||||
/// We try not to duplicate work between client and server builds, like asset copying.
|
||||
pub(crate) fn is_primary_build(&self) -> bool {
|
||||
self.build_id == BuildId::PRIMARY
|
||||
}
|
||||
|
||||
pub(crate) fn profile_phase(&self, label: &'static str) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::ProfilePhase {
|
||||
profile: BuildPhaseProfile {
|
||||
label,
|
||||
start: SystemTime::now(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_wasm_bindgen_start(&self) {
|
||||
self.profile_phase("Wasm Bindgen");
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::RunningBindgen,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_splitting_bundle(&self) {
|
||||
self.profile_phase("Wasm Split");
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::SplittingBundle,
|
||||
});
|
||||
@@ -104,12 +129,14 @@ impl BuildContext {
|
||||
}
|
||||
|
||||
pub(crate) fn status_running_gradle(&self) {
|
||||
self.profile_phase("Gradle");
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::RunningGradle,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn status_compiling_native_plugins(&self, detail: impl Into<String>) {
|
||||
self.profile_phase("Compiling Native Plugins");
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::CompilingNativePlugins {
|
||||
detail: detail.into(),
|
||||
@@ -118,6 +145,7 @@ impl BuildContext {
|
||||
}
|
||||
|
||||
pub(crate) fn status_codesigning(&self) {
|
||||
self.profile_phase("Code Signing");
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::CodeSigning,
|
||||
});
|
||||
@@ -144,6 +172,7 @@ impl BuildContext {
|
||||
name: String,
|
||||
fresh: bool,
|
||||
) {
|
||||
self.profile_phase("Compiling");
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::Compiling {
|
||||
current: count,
|
||||
@@ -155,6 +184,7 @@ impl BuildContext {
|
||||
}
|
||||
|
||||
pub(crate) fn status_starting_build(&self, crate_count: usize) {
|
||||
self.profile_phase("Compiling");
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::Starting {
|
||||
patch: matches!(self.mode, BuildMode::Thin { .. }),
|
||||
@@ -163,19 +193,16 @@ impl BuildContext {
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_starting_link(&self) {
|
||||
pub(crate) fn status_starting_fat_link(&self) {
|
||||
self.profile_phase("Fat Linking");
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::Linking,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_copied_asset(
|
||||
progress: &UnboundedSender<BuilderUpdate>,
|
||||
current: usize,
|
||||
total: usize,
|
||||
path: PathBuf,
|
||||
) {
|
||||
_ = progress.unbounded_send(BuilderUpdate::Progress {
|
||||
pub(crate) fn status_copied_asset(&self, current: usize, total: usize, path: PathBuf) {
|
||||
self.profile_phase("Copying Assets");
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::CopyingAssets {
|
||||
current,
|
||||
total,
|
||||
@@ -185,29 +212,34 @@ impl BuildContext {
|
||||
}
|
||||
|
||||
pub(crate) fn status_optimizing_wasm(&self) {
|
||||
self.profile_phase("Optimizing Wasm");
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::OptimizingWasm,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_hotpatching(&self) {
|
||||
pub(crate) fn status_writing_patch(&self) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::Hotpatching,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_installing_tooling(&self) {
|
||||
self.profile_phase("Installing Tooling");
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::InstallingTooling,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_compressing_assets(&self) {
|
||||
self.profile_phase("Compressing Assets");
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::CompressingAssets,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_extracting_assets(&self) {
|
||||
self.profile_phase("Extracting assets");
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::ExtractingAssets,
|
||||
});
|
||||
|
||||
@@ -1,915 +0,0 @@
|
||||
//! iOS/macOS Swift package manifest helpers and compilation.
|
||||
|
||||
use crate::Result;
|
||||
use anyhow::Context;
|
||||
use manganis_core::SwiftPackageMetadata;
|
||||
use std::path::{Path, PathBuf};
|
||||
use target_lexicon::{OperatingSystem, Triple};
|
||||
use tokio::process::Command;
|
||||
|
||||
/// Create a proper framework bundle from a dylib for iOS/macOS.
|
||||
///
|
||||
/// iOS uses a flat structure while macOS uses a versioned structure.
|
||||
/// Both require an Info.plist for proper App Store submission.
|
||||
pub async fn create_framework_bundle(
|
||||
dylib_path: &Path,
|
||||
framework_name: &str,
|
||||
output_dir: &Path,
|
||||
target_triple: &Triple,
|
||||
bundle_identifier: &str,
|
||||
) -> Result<PathBuf> {
|
||||
let is_ios = matches!(target_triple.operating_system, OperatingSystem::IOS(_));
|
||||
let min_os_version = if is_ios { "13.0" } else { "11.0" };
|
||||
|
||||
let framework_dir = output_dir.join(format!("{}.framework", framework_name));
|
||||
|
||||
// Remove existing framework if present
|
||||
if framework_dir.exists() {
|
||||
std::fs::remove_dir_all(&framework_dir)?;
|
||||
}
|
||||
|
||||
if is_ios {
|
||||
// iOS uses flat structure: Framework.framework/FrameworkName + Info.plist
|
||||
std::fs::create_dir_all(&framework_dir)?;
|
||||
|
||||
// Copy dylib as the framework executable (no extension)
|
||||
let exec_path = framework_dir.join(framework_name);
|
||||
std::fs::copy(dylib_path, &exec_path)?;
|
||||
|
||||
// Set the install name using install_name_tool
|
||||
let output = Command::new("xcrun")
|
||||
.arg("install_name_tool")
|
||||
.arg("-id")
|
||||
.arg(format!(
|
||||
"@rpath/{}.framework/{}",
|
||||
framework_name, framework_name
|
||||
))
|
||||
.arg(&exec_path)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("install_name_tool failed: {}", stderr);
|
||||
}
|
||||
|
||||
// Create Info.plist
|
||||
let info_plist = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{framework_name}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{bundle_identifier}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{framework_name}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>{min_os_version}</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>"#
|
||||
);
|
||||
|
||||
std::fs::write(framework_dir.join("Info.plist"), info_plist)?;
|
||||
} else {
|
||||
// macOS uses versioned structure with symlinks
|
||||
let versions_a = framework_dir.join("Versions").join("A");
|
||||
let resources_dir = versions_a.join("Resources");
|
||||
std::fs::create_dir_all(&resources_dir)?;
|
||||
|
||||
// Copy dylib as the framework executable
|
||||
let exec_path = versions_a.join(framework_name);
|
||||
std::fs::copy(dylib_path, &exec_path)?;
|
||||
|
||||
// Set install name
|
||||
let output = Command::new("xcrun")
|
||||
.arg("install_name_tool")
|
||||
.arg("-id")
|
||||
.arg(format!(
|
||||
"@rpath/{}.framework/Versions/A/{}",
|
||||
framework_name, framework_name
|
||||
))
|
||||
.arg(&exec_path)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("install_name_tool failed: {}", stderr);
|
||||
}
|
||||
|
||||
// Create Info.plist in Resources
|
||||
let info_plist = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{framework_name}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{bundle_identifier}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{framework_name}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>{min_os_version}</string>
|
||||
</dict>
|
||||
</plist>"#
|
||||
);
|
||||
|
||||
std::fs::write(resources_dir.join("Info.plist"), info_plist)?;
|
||||
|
||||
// Create symbolic links (required for macOS framework structure)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let versions_dir = framework_dir.join("Versions");
|
||||
std::os::unix::fs::symlink("A", versions_dir.join("Current"))?;
|
||||
std::os::unix::fs::symlink(
|
||||
format!("Versions/Current/{}", framework_name),
|
||||
framework_dir.join(framework_name),
|
||||
)?;
|
||||
std::os::unix::fs::symlink(
|
||||
"Versions/Current/Resources",
|
||||
framework_dir.join("Resources"),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"Created {} framework bundle: {}",
|
||||
if is_ios { "iOS" } else { "macOS" },
|
||||
framework_dir.display()
|
||||
);
|
||||
|
||||
Ok(framework_dir)
|
||||
}
|
||||
|
||||
/// Compile Swift sources and return the path to the dynamic framework bundle.
|
||||
///
|
||||
/// This function:
|
||||
/// 1. Generates an umbrella Package.swift that includes all Swift plugins
|
||||
/// 2. Runs `swift build` to compile into a dynamic library
|
||||
/// 3. Wraps the dylib in a proper .framework bundle for iOS/macOS
|
||||
/// 4. Returns the path to the resulting `.framework` bundle
|
||||
pub async fn compile_swift_sources(
|
||||
swift_sources: &[SwiftPackageMetadata],
|
||||
target_triple: &Triple,
|
||||
build_dir: &Path,
|
||||
release: bool,
|
||||
) -> Result<Option<PathBuf>> {
|
||||
if swift_sources.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"Compiling {} Swift plugin(s) for {}",
|
||||
swift_sources.len(),
|
||||
target_triple
|
||||
);
|
||||
|
||||
// Create the plugins build directory
|
||||
let plugins_dir = build_dir.join("swift-plugins");
|
||||
std::fs::create_dir_all(&plugins_dir)?;
|
||||
|
||||
// Copy and prepare all Swift source packages
|
||||
let mut plugin_paths = Vec::new();
|
||||
for source in swift_sources {
|
||||
let source_path = PathBuf::from(source.package_path.as_str());
|
||||
let plugin_name = source.plugin_name.as_str();
|
||||
let product_name = source.product.as_str();
|
||||
|
||||
if !source_path.exists() {
|
||||
tracing::warn!(
|
||||
"Swift package path does not exist: {} (for plugin {})",
|
||||
source_path.display(),
|
||||
plugin_name
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let dest_path = plugins_dir.join(plugin_name);
|
||||
if dest_path.exists() {
|
||||
std::fs::remove_dir_all(&dest_path)?;
|
||||
}
|
||||
copy_dir_recursive(&source_path, &dest_path)?;
|
||||
|
||||
// Modify the Package.swift to produce a dynamic library
|
||||
if let Err(e) = modify_package_for_dynamic_library(&dest_path, product_name) {
|
||||
tracing::warn!("Failed to modify Package.swift for dynamic library: {}", e);
|
||||
}
|
||||
|
||||
plugin_paths.push((plugin_name.to_string(), product_name.to_string(), dest_path));
|
||||
tracing::debug!(
|
||||
"Copied Swift plugin '{}' from {} to {}",
|
||||
plugin_name,
|
||||
source_path.display(),
|
||||
plugins_dir.join(plugin_name).display()
|
||||
);
|
||||
}
|
||||
|
||||
if plugin_paths.is_empty() {
|
||||
tracing::warn!("No valid Swift packages found to compile");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Determine Swift target triple and SDK
|
||||
let (swift_triple, sdk_name) = swift_target_and_sdk(target_triple)?;
|
||||
let sdk_path = lookup_sdk_path(&sdk_name).await?;
|
||||
|
||||
// Build configuration
|
||||
let configuration = if release { "release" } else { "debug" };
|
||||
|
||||
// Build each plugin package individually
|
||||
for (plugin_name, product_name, package_path) in &plugin_paths {
|
||||
tracing::debug!(
|
||||
"Building Swift plugin '{}' (product: {})",
|
||||
plugin_name,
|
||||
product_name
|
||||
);
|
||||
|
||||
let build_path = package_path.join(".build");
|
||||
|
||||
let mut cmd = Command::new("xcrun");
|
||||
cmd.args(["swift", "build"])
|
||||
.arg("--package-path")
|
||||
.arg(package_path)
|
||||
.arg("--configuration")
|
||||
.arg(configuration)
|
||||
.arg("--triple")
|
||||
.arg(&swift_triple)
|
||||
.arg("--sdk")
|
||||
.arg(&sdk_path)
|
||||
.arg("--product")
|
||||
.arg(product_name)
|
||||
.arg("--build-path")
|
||||
.arg(&build_path);
|
||||
|
||||
tracing::debug!("Running: xcrun swift build for {}", product_name);
|
||||
|
||||
let output = cmd.output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
anyhow::bail!(
|
||||
"Swift build failed for plugin '{}':\n{}\n{}",
|
||||
plugin_name,
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
}
|
||||
|
||||
if !output.stderr.is_empty() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
tracing::debug!("Swift build warnings for {}:\n{}", plugin_name, stderr);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the output dynamic library for each plugin
|
||||
// Swift puts the output in .build/<triple>/<configuration>/lib<ProductName>.dylib
|
||||
// or .build/<configuration>/lib<ProductName>.dylib depending on the version
|
||||
let mut all_dylibs = Vec::new();
|
||||
|
||||
for (_, product_name, package_path) in &plugin_paths {
|
||||
let build_path = package_path.join(".build");
|
||||
let lib_name = format!("lib{}.dylib", product_name);
|
||||
|
||||
let lib_search_paths = [
|
||||
build_path.join(&swift_triple).join(configuration),
|
||||
build_path.join(configuration),
|
||||
build_path.clone(),
|
||||
];
|
||||
|
||||
let mut found = false;
|
||||
for search_path in &lib_search_paths {
|
||||
let lib_path = search_path.join(&lib_name);
|
||||
if lib_path.exists() {
|
||||
tracing::debug!("Found Swift dynamic library: {}", lib_path.display());
|
||||
all_dylibs.push((product_name.clone(), lib_path));
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
tracing::warn!(
|
||||
"Could not find compiled Swift dynamic library for product '{}' (expected {})",
|
||||
product_name,
|
||||
lib_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if all_dylibs.is_empty() {
|
||||
tracing::warn!("No Swift dynamic libraries were compiled successfully");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// For dynamic libraries, we need to wrap each in a framework bundle
|
||||
// If there's only one library, create a single framework
|
||||
// If there are multiple, we'll create frameworks for each (they're independent)
|
||||
// The first one is the "primary" framework that gets returned
|
||||
|
||||
let (_primary_name, primary_dylib) = all_dylibs.remove(0);
|
||||
|
||||
// Create the framework bundle from the dylib
|
||||
// Use "DioxusSwiftPlugins" as the umbrella framework name
|
||||
let framework_name = "DioxusSwiftPlugins";
|
||||
let bundle_identifier = "com.dioxus.swift.plugins";
|
||||
|
||||
let framework_path = create_framework_bundle(
|
||||
&primary_dylib,
|
||||
framework_name,
|
||||
build_dir,
|
||||
target_triple,
|
||||
bundle_identifier,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// If there are additional dylibs, create separate framework bundles for them
|
||||
for (name, dylib_path) in all_dylibs {
|
||||
let extra_framework = create_framework_bundle(
|
||||
&dylib_path,
|
||||
&name,
|
||||
build_dir,
|
||||
target_triple,
|
||||
&format!("com.dioxus.swift.{}", name.to_lowercase()),
|
||||
)
|
||||
.await?;
|
||||
tracing::debug!(
|
||||
"Created additional framework: {}",
|
||||
extra_framework.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Some(framework_path))
|
||||
}
|
||||
|
||||
/// Modify a Package.swift to produce a dynamic library instead of static.
|
||||
/// This is needed for runtime class lookup via NSClassFromString.
|
||||
fn modify_package_for_dynamic_library(package_path: &Path, product_name: &str) -> Result<()> {
|
||||
let package_swift_path = package_path.join("Package.swift");
|
||||
if !package_swift_path.exists() {
|
||||
anyhow::bail!(
|
||||
"Package.swift not found at {}",
|
||||
package_swift_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&package_swift_path)?;
|
||||
|
||||
// Replace .static with .dynamic for the library type
|
||||
let modified = content
|
||||
.replace("type: .static", "type: .dynamic")
|
||||
.replace("type:.static", "type: .dynamic");
|
||||
|
||||
// If no library type was specified, we need to add it
|
||||
// Look for .library(name: "ProductName", targets: [...]) and change to
|
||||
// .library(name: "ProductName", type: .dynamic, targets: [...])
|
||||
let pattern = format!(
|
||||
r#".library\s*\(\s*name\s*:\s*"{}"\s*,\s*targets"#,
|
||||
regex::escape(product_name)
|
||||
);
|
||||
let replacement = format!(
|
||||
r#".library(name: "{}", type: .dynamic, targets"#,
|
||||
product_name
|
||||
);
|
||||
|
||||
let modified = if let Ok(re) = regex::Regex::new(&pattern) {
|
||||
re.replace_all(&modified, replacement.as_str()).to_string()
|
||||
} else {
|
||||
modified
|
||||
};
|
||||
|
||||
std::fs::write(&package_swift_path, modified)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert a Rust target triple to Swift target triple and SDK name
|
||||
fn swift_target_and_sdk(triple: &Triple) -> Result<(String, String)> {
|
||||
use target_lexicon::{Architecture, Environment, OperatingSystem};
|
||||
|
||||
// Check if this is a simulator target using the environment field
|
||||
let is_simulator = triple.environment == Environment::Sim;
|
||||
|
||||
let swift_triple = match (&triple.architecture, &triple.operating_system) {
|
||||
(Architecture::Aarch64(_), OperatingSystem::IOS(_)) => {
|
||||
if is_simulator {
|
||||
"arm64-apple-ios-simulator"
|
||||
} else {
|
||||
"arm64-apple-ios"
|
||||
}
|
||||
}
|
||||
(Architecture::Aarch64(_), OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_)) => {
|
||||
"arm64-apple-macosx"
|
||||
}
|
||||
(Architecture::X86_64, OperatingSystem::IOS(_)) => "x86_64-apple-ios-simulator",
|
||||
(Architecture::X86_64, OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_)) => {
|
||||
"x86_64-apple-macosx"
|
||||
}
|
||||
_ => anyhow::bail!("Unsupported target for Swift compilation: {}", triple),
|
||||
};
|
||||
|
||||
let sdk_name = match &triple.operating_system {
|
||||
OperatingSystem::IOS(_) => {
|
||||
// Check if this is a simulator target using the environment field
|
||||
if is_simulator {
|
||||
"iphonesimulator"
|
||||
} else {
|
||||
"iphoneos"
|
||||
}
|
||||
}
|
||||
OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_) => "macosx",
|
||||
_ => anyhow::bail!(
|
||||
"Unsupported operating system for Swift compilation: {:?}",
|
||||
triple.operating_system
|
||||
),
|
||||
};
|
||||
|
||||
Ok((swift_triple.to_string(), sdk_name.to_string()))
|
||||
}
|
||||
|
||||
/// Look up the SDK path using xcrun
|
||||
async fn lookup_sdk_path(sdk_name: &str) -> Result<String> {
|
||||
let output = Command::new("xcrun")
|
||||
.args(["--sdk", sdk_name, "--show-sdk-path"])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to run xcrun to find SDK path")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("Failed to find SDK '{}': {}", sdk_name, stderr);
|
||||
}
|
||||
|
||||
let sdk_path = String::from_utf8(output.stdout)
|
||||
.context("Invalid UTF-8 in SDK path")?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if sdk_path.is_empty() {
|
||||
anyhow::bail!("SDK path for '{}' is empty", sdk_name);
|
||||
}
|
||||
|
||||
Ok(sdk_path)
|
||||
}
|
||||
|
||||
/// Recursively copy a directory
|
||||
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
|
||||
std::fs::create_dir_all(dst)?;
|
||||
|
||||
for entry in std::fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let ty = entry.file_type()?;
|
||||
let src_path = entry.path();
|
||||
let dst_path = dst.join(entry.file_name());
|
||||
|
||||
if ty.is_dir() {
|
||||
// Skip .build directories
|
||||
if entry.file_name() == ".build" {
|
||||
continue;
|
||||
}
|
||||
copy_dir_recursive(&src_path, &dst_path)?;
|
||||
} else {
|
||||
std::fs::copy(&src_path, &dst_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract Swift metadata from object files in link arguments
|
||||
pub fn extract_swift_metadata_from_link_args(
|
||||
link_args: &[String],
|
||||
workspace_dir: &Path,
|
||||
) -> Vec<SwiftPackageMetadata> {
|
||||
let mut swift_packages = Vec::new();
|
||||
|
||||
// Look through rlibs and object files for Swift metadata
|
||||
for arg in link_args {
|
||||
let path = PathBuf::from(arg);
|
||||
|
||||
// Only process files in our workspace
|
||||
if !path.starts_with(workspace_dir) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for .rlib files
|
||||
if arg.ends_with(".rlib") {
|
||||
if let Ok(swift_meta) = extract_swift_from_rlib(&path) {
|
||||
swift_packages.extend(swift_meta);
|
||||
}
|
||||
}
|
||||
// Check for .o files
|
||||
else if arg.ends_with(".o") || arg.ends_with(".obj") {
|
||||
if let Ok(swift_meta) = extract_swift_from_object(&path) {
|
||||
swift_packages.extend(swift_meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate by plugin name
|
||||
swift_packages.sort_by(|a, b| a.plugin_name.as_str().cmp(b.plugin_name.as_str()));
|
||||
swift_packages.dedup_by(|a, b| a.plugin_name.as_str() == b.plugin_name.as_str());
|
||||
|
||||
swift_packages
|
||||
}
|
||||
|
||||
/// Extract Swift metadata from an rlib file
|
||||
fn extract_swift_from_rlib(rlib_path: &Path) -> Result<Vec<SwiftPackageMetadata>> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
let rlib_contents = std::fs::read(rlib_path)?;
|
||||
let mut reader = ar::Archive::new(std::io::Cursor::new(rlib_contents));
|
||||
|
||||
while let Some(Ok(entry)) = reader.next_entry() {
|
||||
let name = std::str::from_utf8(entry.header().identifier()).unwrap_or_default();
|
||||
|
||||
// Only process .o files
|
||||
if !name.ends_with(".rcgu.o") && !name.ends_with(".obj") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read the object file contents
|
||||
let mut obj_contents = Vec::new();
|
||||
std::io::Read::read_to_end(&mut std::io::BufReader::new(entry), &mut obj_contents)?;
|
||||
|
||||
if let Ok(swift_meta) = extract_swift_from_bytes(&obj_contents) {
|
||||
results.extend(swift_meta);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Extract Swift metadata from an object file
|
||||
fn extract_swift_from_object(obj_path: &Path) -> Result<Vec<SwiftPackageMetadata>> {
|
||||
let obj_contents = std::fs::read(obj_path)?;
|
||||
extract_swift_from_bytes(&obj_contents)
|
||||
}
|
||||
|
||||
/// Extract Swift metadata from raw object file bytes
|
||||
fn extract_swift_from_bytes(bytes: &[u8]) -> Result<Vec<SwiftPackageMetadata>> {
|
||||
use manganis_core::SymbolData;
|
||||
use object::{Object, ObjectSection, ObjectSymbol};
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
let file = match object::File::parse(bytes) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return Ok(results),
|
||||
};
|
||||
|
||||
// Look for __ASSETS__ symbols
|
||||
for symbol in file.symbols() {
|
||||
let name = match symbol.name() {
|
||||
Ok(n) => n,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if !name.starts_with("__ASSETS__") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to get the symbol's data
|
||||
if let Some(section_idx) = symbol.section().index() {
|
||||
if let Ok(section) = file.section_by_index(section_idx) {
|
||||
if let Ok(data) = section.data() {
|
||||
// Try to find the symbol data in the section
|
||||
let addr = symbol.address();
|
||||
let section_addr = section.address();
|
||||
let offset = (addr - section_addr) as usize;
|
||||
|
||||
if offset < data.len() {
|
||||
let symbol_data = &data[offset..];
|
||||
// Try to deserialize as SymbolData
|
||||
if let Some((_, SymbolData::SwiftPackage(meta))) =
|
||||
const_serialize::deserialize_const!(SymbolData, symbol_data)
|
||||
{
|
||||
results.push(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Recursively collect all Swift source files from a directory
|
||||
fn collect_swift_files(dir: &Path) -> Result<Vec<PathBuf>> {
|
||||
let mut swift_files = Vec::new();
|
||||
|
||||
if !dir.exists() {
|
||||
return Ok(swift_files);
|
||||
}
|
||||
|
||||
for entry in std::fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
// Recursively collect from subdirectories
|
||||
swift_files.extend(collect_swift_files(&path)?);
|
||||
} else if path.extension().is_some_and(|ext| ext == "swift") {
|
||||
swift_files.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(swift_files)
|
||||
}
|
||||
|
||||
/// Information about an Apple Widget Extension to compile
|
||||
pub struct AppleWidgetSource {
|
||||
/// Path to the Swift package source directory
|
||||
pub source_path: PathBuf,
|
||||
/// Display name for the widget (shown in system UI)
|
||||
pub display_name: String,
|
||||
/// Bundle ID suffix (appended to app bundle ID)
|
||||
pub bundle_id_suffix: String,
|
||||
/// Minimum deployment target (e.g., "16.0")
|
||||
pub deployment_target: String,
|
||||
/// Swift module name for the widget.
|
||||
/// This MUST match the module name used by the main app's Swift plugin
|
||||
/// for ActivityKit type matching to work (e.g., both must define
|
||||
/// `ModuleName.LocationPermissionAttributes` as the same type).
|
||||
pub module_name: String,
|
||||
}
|
||||
|
||||
/// Compile an Apple Widget Extension from a Swift package source.
|
||||
///
|
||||
/// Widget Extensions are compiled as executables (not libraries) and bundled
|
||||
/// as .appex bundles which are installed in the app's PlugIns folder.
|
||||
///
|
||||
/// **Important**: Widget extensions are XPC services that require special initialization.
|
||||
/// We use `-e _NSExtensionMain` as the entry point instead of the default `_main` that
|
||||
/// Swift generates with `@main`. The `_NSExtensionMain` entry point (provided by Foundation):
|
||||
/// 1. Sets up the XPC listener
|
||||
/// 2. Initializes ExtensionFoundation's `_EXRunningExtension` singleton
|
||||
/// 3. Registers with PlugInKit
|
||||
/// 4. Then calls your Widget code
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `widget` - Widget extension source configuration
|
||||
/// * `target_triple` - The target platform (e.g., aarch64-apple-ios)
|
||||
/// * `build_dir` - Directory for intermediate build files
|
||||
/// * `app_bundle_id` - The main app's bundle identifier (widget ID is derived from this)
|
||||
/// * `release` - Whether to build in release mode
|
||||
///
|
||||
/// # Returns
|
||||
/// Path to the compiled .appex bundle, ready to be installed to PlugIns/
|
||||
pub async fn compile_apple_widget(
|
||||
widget: &AppleWidgetSource,
|
||||
target_triple: &Triple,
|
||||
build_dir: &Path,
|
||||
app_bundle_id: &str,
|
||||
release: bool,
|
||||
) -> Result<PathBuf> {
|
||||
use target_lexicon::OperatingSystem;
|
||||
|
||||
// Validate we're on an Apple platform
|
||||
let is_ios = matches!(target_triple.operating_system, OperatingSystem::IOS(_));
|
||||
let is_macos = matches!(
|
||||
target_triple.operating_system,
|
||||
OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin(_)
|
||||
);
|
||||
|
||||
if !is_ios && !is_macos {
|
||||
anyhow::bail!(
|
||||
"Apple Widget Extensions are only supported on iOS and macOS, not {:?}",
|
||||
target_triple.operating_system
|
||||
);
|
||||
}
|
||||
|
||||
// Validate source path exists
|
||||
if !widget.source_path.exists() {
|
||||
anyhow::bail!(
|
||||
"Widget Extension source path does not exist: {}",
|
||||
widget.source_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"Compiling Apple Widget Extension '{}' for {}",
|
||||
widget.display_name,
|
||||
target_triple
|
||||
);
|
||||
|
||||
// Create the widget build directory
|
||||
let widget_build_dir = build_dir.join("widget-extensions");
|
||||
std::fs::create_dir_all(&widget_build_dir)?;
|
||||
|
||||
// Copy the Swift package to build directory
|
||||
// Use the bundle_id_suffix as a unique name since the folder name might just be "widget"
|
||||
let widget_name = widget.bundle_id_suffix.replace("-", "_");
|
||||
let source_dir = widget_build_dir.join(format!("{}_src", widget_name));
|
||||
if source_dir.exists() {
|
||||
std::fs::remove_dir_all(&source_dir)?;
|
||||
}
|
||||
copy_dir_recursive(&widget.source_path, &source_dir)?;
|
||||
|
||||
// Get Swift target triple and SDK
|
||||
let (swift_triple, sdk_name) = swift_target_and_sdk(target_triple)?;
|
||||
|
||||
// Collect all Swift source files from the Sources directory
|
||||
let swift_sources_dir = source_dir.join("Sources");
|
||||
let swift_files = collect_swift_files(&swift_sources_dir)?;
|
||||
|
||||
if swift_files.is_empty() {
|
||||
anyhow::bail!(
|
||||
"No Swift source files found in widget extension Sources directory: {}",
|
||||
swift_sources_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"Found {} Swift files for widget: {:?}",
|
||||
swift_files.len(),
|
||||
swift_files
|
||||
);
|
||||
|
||||
// Build output path
|
||||
let exec_path = widget_build_dir.join(&widget_name);
|
||||
|
||||
// Compile the widget extension using swiftc directly
|
||||
// Widget extensions are XPC services that require _NSExtensionMain as the entry point
|
||||
let mut cmd = Command::new("xcrun");
|
||||
cmd.arg("--sdk").arg(&sdk_name).arg("swiftc");
|
||||
|
||||
// Add all Swift source files
|
||||
for swift_file in &swift_files {
|
||||
cmd.arg(swift_file);
|
||||
}
|
||||
|
||||
// Output executable
|
||||
cmd.arg("-o").arg(&exec_path);
|
||||
|
||||
// Target triple with proper iOS version
|
||||
// Format: arm64-apple-ios17.0 or arm64-apple-ios17.0-simulator
|
||||
let is_simulator = swift_triple.contains("simulator");
|
||||
let base_triple = swift_triple.replace("-simulator", "");
|
||||
let swift_target = if is_simulator {
|
||||
format!("{}{}-simulator", base_triple, widget.deployment_target)
|
||||
} else {
|
||||
format!("{}{}", base_triple, widget.deployment_target)
|
||||
};
|
||||
cmd.arg("-target").arg(&swift_target);
|
||||
|
||||
// Module name - use a consistent name that matches the main app's plugin module
|
||||
// This is critical for ActivityKit type matching between app and widget
|
||||
cmd.arg("-module-name").arg(&widget.module_name);
|
||||
|
||||
// Optimization flags
|
||||
if release {
|
||||
cmd.arg("-O").arg("-whole-module-optimization");
|
||||
}
|
||||
|
||||
// Extension-specific flags
|
||||
cmd.arg("-application-extension");
|
||||
|
||||
// Critical: Use _NSExtensionMain as the entry point for widget extensions
|
||||
// Without this, the widget crashes because ExtensionFoundation's singleton isn't initialized
|
||||
cmd.arg("-Xlinker")
|
||||
.arg("-e")
|
||||
.arg("-Xlinker")
|
||||
.arg("_NSExtensionMain");
|
||||
|
||||
// Link Objective-C runtime (required for Swift/ObjC interop)
|
||||
cmd.arg("-lobjc");
|
||||
|
||||
// Link required frameworks
|
||||
cmd.arg("-framework").arg("Foundation");
|
||||
cmd.arg("-framework").arg("SwiftUI");
|
||||
cmd.arg("-framework").arg("WidgetKit");
|
||||
cmd.arg("-framework").arg("ActivityKit");
|
||||
|
||||
tracing::debug!("Running swiftc for widget: {:?}", cmd);
|
||||
|
||||
let output = cmd.output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
anyhow::bail!(
|
||||
"Swift compilation failed for widget extension '{}':\n{}\n{}",
|
||||
widget_name,
|
||||
stdout,
|
||||
stderr
|
||||
);
|
||||
}
|
||||
|
||||
tracing::debug!("Compiled widget executable: {}", exec_path.display());
|
||||
|
||||
// Create the .appex bundle
|
||||
let appex_name = format!("{}.appex", widget_name);
|
||||
let appex_dir = widget_build_dir.join(&appex_name);
|
||||
|
||||
// Remove existing appex if present
|
||||
if appex_dir.exists() {
|
||||
std::fs::remove_dir_all(&appex_dir)?;
|
||||
}
|
||||
std::fs::create_dir_all(&appex_dir)?;
|
||||
|
||||
// Copy the executable into the appex bundle
|
||||
let bundle_exec = appex_dir.join(&widget_name);
|
||||
std::fs::copy(&exec_path, &bundle_exec)?;
|
||||
|
||||
// Create Info.plist for the widget extension
|
||||
let widget_bundle_id = format!("{}.{}", app_bundle_id, widget.bundle_id_suffix);
|
||||
let min_os_version = &widget.deployment_target;
|
||||
|
||||
let platform_info = if is_ios {
|
||||
format!(
|
||||
r#" <key>MinimumOSVersion</key>
|
||||
<string>{min_os_version}</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>1</integer>
|
||||
<integer>2</integer>
|
||||
</array>"#
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#" <key>LSMinimumSystemVersion</key>
|
||||
<string>{min_os_version}</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>"#
|
||||
)
|
||||
};
|
||||
|
||||
let info_plist = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>{display_name}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{widget_name}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{widget_bundle_id}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{widget_name}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
{platform_info}
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>"#,
|
||||
display_name = widget.display_name,
|
||||
widget_name = widget_name,
|
||||
widget_bundle_id = widget_bundle_id,
|
||||
platform_info = platform_info,
|
||||
);
|
||||
|
||||
std::fs::write(appex_dir.join("Info.plist"), info_plist)?;
|
||||
|
||||
tracing::debug!("Created Widget Extension bundle: {}", appex_dir.display());
|
||||
|
||||
Ok(appex_dir)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,24 +0,0 @@
|
||||
//! The build manifest for `dx` applications, containing metadata about the build including
|
||||
//! the CLI version, Rust version, and all bundled assets.
|
||||
//!
|
||||
//! We eventually plan to use this manifest to support tighter integration with deployment platforms
|
||||
//! and CDNs.
|
||||
//!
|
||||
//! This manifest contains the list of assets, rust version, and cli version used to build the app.
|
||||
//! Eventually, we might want to expand this to include more metadata about the build, including
|
||||
//! build time, target platform, etc.
|
||||
|
||||
use crate::opt::AssetManifest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct AppManifest {
|
||||
/// Stable since 0.7.0
|
||||
pub cli_version: String,
|
||||
|
||||
/// Stable since 0.7.0
|
||||
pub rust_version: String,
|
||||
|
||||
/// Stable since 0.7.0
|
||||
pub assets: AssetManifest,
|
||||
}
|
||||
@@ -8,24 +8,22 @@
|
||||
//! hot-patching Rust code through binary analysis and a custom linker. The [`builder`] module contains
|
||||
//! the management of the ongoing build and methods to open the build as a running app.
|
||||
|
||||
// Platform-specific build code
|
||||
mod android;
|
||||
mod apple;
|
||||
mod web;
|
||||
|
||||
mod assets;
|
||||
mod builder;
|
||||
mod cache;
|
||||
mod context;
|
||||
mod ios_swift;
|
||||
mod manifest;
|
||||
mod manifest_mapper;
|
||||
mod link;
|
||||
mod patch;
|
||||
mod pre_render;
|
||||
mod renderer;
|
||||
mod request;
|
||||
mod tools;
|
||||
|
||||
pub(crate) use android::AndroidTools;
|
||||
pub(crate) use assets::*;
|
||||
pub(crate) use builder::*;
|
||||
pub(crate) use cache::*;
|
||||
pub(crate) use context::*;
|
||||
pub(crate) use manifest::*;
|
||||
pub(crate) use patch::*;
|
||||
pub(crate) use pre_render::*;
|
||||
pub(crate) use request::*;
|
||||
pub(crate) use tools::*;
|
||||
@@ -9,7 +9,8 @@ use object::{
|
||||
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
ops::{Deref, Range},
|
||||
io::Read,
|
||||
ops::Range,
|
||||
path::Path,
|
||||
path::PathBuf,
|
||||
sync::{Arc, RwLock},
|
||||
@@ -716,6 +717,21 @@ pub fn create_wasm_jump_table(patch: &Path, cache: &HotpatchModuleCache) -> Resu
|
||||
// Clear the start function from the patch - we don't want any code automatically running!
|
||||
new.start = None;
|
||||
|
||||
// Export __wasm_apply_global_relocs if it exists. wasm-ld generates this synthetic
|
||||
// function to relocate GOT.func.internal globals by __table_base, but refuses to
|
||||
// export it via --export or --export-if-defined since it's not a linker symbol.
|
||||
// Without this export, the runtime can't call it, leaving GOT.func.internal globals
|
||||
// unrelocated — they contain element-segment-relative offsets instead of absolute
|
||||
// table indices, causing call_indirect type mismatches in PIC-compiled workspace code.
|
||||
const APPLY_RELOCS: &str = "__wasm_apply_global_relocs";
|
||||
if let Some(func) = new
|
||||
.funcs
|
||||
.iter()
|
||||
.find(|f| f.name.as_deref() == Some(APPLY_RELOCS))
|
||||
{
|
||||
new.exports.add(APPLY_RELOCS, func.id());
|
||||
}
|
||||
|
||||
// Update the wasm module on the filesystem to use the newly lifted version
|
||||
let lib = patch.to_path_buf();
|
||||
std::fs::write(&lib, new.emit_wasm())?;
|
||||
@@ -846,15 +862,7 @@ pub fn create_undefined_symbol_stub(
|
||||
let mut defined_symbols = HashSet::new();
|
||||
|
||||
for path in sorted {
|
||||
let bytes = std::fs::read(path).with_context(|| format!("failed to read {path:?}"))?;
|
||||
let file = File::parse(bytes.deref() as &[u8])?;
|
||||
for symbol in file.symbols() {
|
||||
if symbol.is_undefined() {
|
||||
undefined_symbols.insert(symbol.name()?.to_string());
|
||||
} else if symbol.is_global() {
|
||||
defined_symbols.insert(symbol.name()?.to_string());
|
||||
}
|
||||
}
|
||||
collect_stub_symbols_from_path(path, &mut undefined_symbols, &mut defined_symbols)?;
|
||||
}
|
||||
let undefined_symbols: Vec<_> = undefined_symbols
|
||||
.difference(&defined_symbols)
|
||||
@@ -1251,6 +1259,54 @@ pub fn create_undefined_symbol_stub(
|
||||
Ok(obj.write()?)
|
||||
}
|
||||
|
||||
fn collect_stub_symbols_from_path(
|
||||
path: &Path,
|
||||
undefined_symbols: &mut HashSet<String>,
|
||||
defined_symbols: &mut HashSet<String>,
|
||||
) -> Result<()> {
|
||||
let bytes = std::fs::read(path).with_context(|| format!("failed to read {path:?}"))?;
|
||||
|
||||
if path
|
||||
.extension()
|
||||
.is_some_and(|ext| matches!(ext.to_str(), Some("rlib" | "a")))
|
||||
{
|
||||
let mut archive = ar::Archive::new(std::io::Cursor::new(bytes));
|
||||
while let Some(entry) = archive.next_entry() {
|
||||
let mut entry = entry?;
|
||||
let name = std::str::from_utf8(entry.header().identifier()).unwrap_or_default();
|
||||
|
||||
if name.ends_with(".rmeta") || !(name.ends_with(".o") || name.ends_with(".obj")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut entry_bytes = Vec::with_capacity(entry.header().size() as usize);
|
||||
entry.read_to_end(&mut entry_bytes)?;
|
||||
collect_stub_symbols_from_bytes(&entry_bytes, undefined_symbols, defined_symbols)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
collect_stub_symbols_from_bytes(&bytes, undefined_symbols, defined_symbols)
|
||||
}
|
||||
|
||||
fn collect_stub_symbols_from_bytes(
|
||||
bytes: &[u8],
|
||||
undefined_symbols: &mut HashSet<String>,
|
||||
defined_symbols: &mut HashSet<String>,
|
||||
) -> Result<()> {
|
||||
let file = File::parse(bytes)?;
|
||||
for symbol in file.symbols() {
|
||||
if symbol.is_undefined() {
|
||||
undefined_symbols.insert(symbol.name()?.to_string());
|
||||
} else if symbol.is_global() {
|
||||
defined_symbols.insert(symbol.name()?.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prepares the base module before running wasm-bindgen.
|
||||
///
|
||||
/// This tries to work around how wasm-bindgen works by intelligently promoting non-wasm-bindgen functions
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
use anyhow::Context;
|
||||
use dioxus_cli_config::{server_ip, server_port};
|
||||
use dioxus_dx_wire_format::BuildStage;
|
||||
use futures_util::{stream::FuturesUnordered, StreamExt};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::BuildId;
|
||||
|
||||
use super::{AppBuilder, BuilderUpdate};
|
||||
|
||||
/// Pre-render the static routes, performing static-site generation
|
||||
pub(crate) async fn pre_render_static_routes(
|
||||
devserver_ip: Option<SocketAddr>,
|
||||
builder: &mut AppBuilder,
|
||||
updates: Option<&futures_channel::mpsc::UnboundedSender<BuilderUpdate>>,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(updates) = updates {
|
||||
updates
|
||||
.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::Prerendering,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
let server_exe = builder.build.main_exe();
|
||||
|
||||
// Use the address passed in through environment variables or default to localhost:9999. We need
|
||||
// to default to a value that is different than the CLI default address to avoid conflicts
|
||||
let ip = server_ip().unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
||||
let port = server_port().unwrap_or(9999);
|
||||
let fullstack_address = SocketAddr::new(ip, port);
|
||||
let address = fullstack_address.ip().to_string();
|
||||
let port = fullstack_address.port().to_string();
|
||||
|
||||
// Borrow port and address so we can easily move them into multiple tasks below
|
||||
let address = &address;
|
||||
let port = &port;
|
||||
|
||||
tracing::info!("Running SSG at http://{address}:{port} for {server_exe:?}");
|
||||
|
||||
let vars = builder.child_environment_variables(
|
||||
devserver_ip,
|
||||
Some(fullstack_address),
|
||||
false,
|
||||
BuildId::SECONDARY,
|
||||
);
|
||||
// Run the server executable
|
||||
let _child = Command::new(&server_exe)
|
||||
.envs(vars)
|
||||
.current_dir(server_exe.parent().unwrap())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
// Borrow reqwest_client so we only move the reference into the futures
|
||||
let reqwest_client = reqwest::Client::new();
|
||||
let reqwest_client = &reqwest_client;
|
||||
|
||||
// Get the routes from the `/static_routes` endpoint
|
||||
let mut routes = None;
|
||||
|
||||
// The server may take a few seconds to start up. Try fetching the route up to 5 times with a one second delay
|
||||
const RETRY_ATTEMPTS: usize = 5;
|
||||
for i in 0..=RETRY_ATTEMPTS {
|
||||
tracing::debug!(
|
||||
"Attempting to get static routes from server. Attempt {i} of {RETRY_ATTEMPTS}"
|
||||
);
|
||||
|
||||
let request = reqwest_client
|
||||
.post(format!("http://{address}:{port}/api/static_routes"))
|
||||
.body("{}".to_string())
|
||||
.send()
|
||||
.await;
|
||||
match request {
|
||||
Ok(request) => {
|
||||
routes = Some(request
|
||||
.json::<Vec<String>>()
|
||||
.await
|
||||
.inspect(|text| tracing::debug!("Got static routes: {text:?}"))
|
||||
.context("Failed to parse static routes from the server. Make sure your server function returns Vec<String> with the (default) json encoding")?);
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
// If the request fails, try up to 5 times with a one second delay
|
||||
// If it fails 5 times, return the error
|
||||
if i == RETRY_ATTEMPTS {
|
||||
return Err(err).context("Failed to get static routes from server. Make sure you have a server function at the `/api/static_routes` endpoint that returns Vec<String> of static routes.");
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let routes = routes.expect(
|
||||
"static routes should exist or an error should have been returned on the last attempt",
|
||||
);
|
||||
|
||||
// Create a pool of futures that cache each route
|
||||
let mut resolved_routes = routes
|
||||
.into_iter()
|
||||
.map(|route| async move {
|
||||
tracing::info!("Rendering {route} for SSG");
|
||||
|
||||
// For each route, ping the server to force it to cache the response for ssg
|
||||
let request = reqwest_client
|
||||
.get(format!("http://{address}:{port}{route}"))
|
||||
.header("Accept", "text/html")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
// If it takes longer than 30 seconds to resolve the route, log a warning
|
||||
let warning_task = tokio::spawn({
|
||||
let route = route.clone();
|
||||
async move {
|
||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
tracing::warn!("Route {route} has been rendering for 30 seconds");
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the streaming response to completely finish before continuing. We don't use the html it returns directly
|
||||
// because it may contain artifacts of intermediate streaming steps while the page is loading. The SSG app should write
|
||||
// the final clean HTML to the disk automatically after the request completes.
|
||||
let _html = request.text().await?;
|
||||
|
||||
// Cancel the warning task if it hasn't already run
|
||||
warning_task.abort();
|
||||
|
||||
Ok::<_, reqwest::Error>(route)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
while let Some(route) = resolved_routes.next().await {
|
||||
match route {
|
||||
Ok(route) => tracing::debug!("ssg success: {route:?}"),
|
||||
Err(err) => tracing::error!("ssg error: {err:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("SSG complete");
|
||||
|
||||
drop(_child);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
use crate::Renderer;
|
||||
use target_lexicon::Triple;
|
||||
|
||||
use crate::BuildRequest;
|
||||
|
||||
impl BuildRequest {
|
||||
pub(crate) fn renderer_enabled_by_dioxus_dependency(
|
||||
package: &krates::cm::Package,
|
||||
) -> Option<(Renderer, String)> {
|
||||
let mut renderers = vec![];
|
||||
|
||||
// Attempt to discover the platform directly from the dioxus dependency
|
||||
//
|
||||
// [dependencies]
|
||||
// dioxus = { features = ["web"] }
|
||||
//
|
||||
if let Some(dxs) = package.dependencies.iter().find(|dep| dep.name == "dioxus") {
|
||||
for feature in dxs.features.iter() {
|
||||
if let Some(renderer) = Renderer::autodetect_from_cargo_feature(feature) {
|
||||
renderers.push((renderer, format!("dioxus/{}", feature)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if renderers.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(renderers[0].clone())
|
||||
}
|
||||
|
||||
pub(crate) fn features_that_enable_renderers(
|
||||
package: &krates::cm::Package,
|
||||
) -> Vec<(Renderer, String)> {
|
||||
package
|
||||
.features
|
||||
.keys()
|
||||
.filter_map(|key| {
|
||||
Renderer::autodetect_from_cargo_feature(key).map(|v| (v, key.to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Return the platforms that are enabled for the package only from the default features
|
||||
///
|
||||
/// Ideally only one platform is enabled but we need to be able to
|
||||
pub(crate) fn enabled_cargo_toml_default_features_renderers(
|
||||
package: &krates::cm::Package,
|
||||
) -> Vec<(Renderer, String)> {
|
||||
let mut renderers = vec![];
|
||||
|
||||
// Start searching through the default features
|
||||
//
|
||||
// [features]
|
||||
// default = ["dioxus/web"]
|
||||
//
|
||||
// or
|
||||
//
|
||||
// [features]
|
||||
// default = ["web"]
|
||||
// web = ["dioxus/web"]
|
||||
let Some(default) = package.features.get("default") else {
|
||||
return renderers;
|
||||
};
|
||||
|
||||
// we only trace features 1 level deep..
|
||||
// TODO: trace all enabled features, not just default features
|
||||
for feature in default.iter() {
|
||||
// If the user directly specified a platform we can just use that.
|
||||
if feature.starts_with("dioxus/") {
|
||||
let dx_feature = feature.trim_start_matches("dioxus/");
|
||||
let auto = Renderer::autodetect_from_cargo_feature(dx_feature);
|
||||
if let Some(auto) = auto {
|
||||
renderers.push((auto, dx_feature.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// If the user is specifying an internal feature that points to a platform, we can use that
|
||||
let internal_feature = package.features.get(feature);
|
||||
if let Some(internal_feature) = internal_feature {
|
||||
for feature in internal_feature {
|
||||
if feature.starts_with("dioxus/") {
|
||||
let dx_feature = feature.trim_start_matches("dioxus/");
|
||||
let auto = Renderer::autodetect_from_cargo_feature(dx_feature);
|
||||
if let Some(auto) = auto {
|
||||
renderers.push((auto, dx_feature.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderers.sort();
|
||||
renderers.dedup();
|
||||
|
||||
renderers
|
||||
}
|
||||
|
||||
/// Gather the features that are enabled for the package
|
||||
pub fn rendererless_features(package: &krates::cm::Package) -> Vec<String> {
|
||||
let Some(default) = package.features.get("default") else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut kept_features = vec![];
|
||||
|
||||
// Only keep the top-level features in the default list that don't point to a platform directly
|
||||
// IE we want to drop `web` if default = ["web"]
|
||||
'top: for feature in default {
|
||||
// Don't keep features that point to a platform via dioxus/blah
|
||||
if feature.starts_with("dioxus/") {
|
||||
let dx_feature = feature.trim_start_matches("dioxus/");
|
||||
if Renderer::autodetect_from_cargo_feature(dx_feature).is_some() {
|
||||
tracing::debug!(
|
||||
"Dropping feature {feature} since it points to a platform renderer"
|
||||
);
|
||||
continue 'top;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't keep features that point to a platform via an internal feature
|
||||
if let Some(internal_feature) = package.features.get(feature) {
|
||||
for feature in internal_feature {
|
||||
if feature.starts_with("dioxus/") {
|
||||
let dx_feature = feature.trim_start_matches("dioxus/");
|
||||
if Renderer::autodetect_from_cargo_feature(dx_feature).is_some() {
|
||||
tracing::debug!(
|
||||
"Dropping feature {feature} since it points to a platform renderer transitively"
|
||||
);
|
||||
continue 'top;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise we can keep it
|
||||
kept_features.push(feature.to_string());
|
||||
}
|
||||
|
||||
kept_features
|
||||
}
|
||||
|
||||
/// Get the features required to build for the given platform
|
||||
pub fn feature_for_platform_and_renderer(
|
||||
package: &krates::cm::Package,
|
||||
triple: &Triple,
|
||||
renderer: Renderer,
|
||||
) -> Option<String> {
|
||||
// Try to find the feature that activates the dioxus feature for the given platform
|
||||
let dioxus_feature = renderer.feature_name(triple);
|
||||
|
||||
let res = package.features.iter().find_map(|(key, features)| {
|
||||
// if the feature is just the name of the platform, we use that
|
||||
if key == dioxus_feature {
|
||||
tracing::debug!("Found feature {key} for renderer {renderer}");
|
||||
return Some(key.clone());
|
||||
}
|
||||
|
||||
// Otherwise look for the feature that starts with dioxus/ or dioxus?/ and matches just the single platform
|
||||
// we are looking for.
|
||||
let mut dioxus_renderers_enabled = Vec::new();
|
||||
for feature in features {
|
||||
if let Some((_, after_dioxus)) = feature.split_once("dioxus") {
|
||||
if let Some(dioxus_feature_enabled) =
|
||||
after_dioxus.trim_start_matches('?').strip_prefix('/')
|
||||
{
|
||||
if Renderer::autodetect_from_cargo_feature(dioxus_feature_enabled).is_some()
|
||||
{
|
||||
dioxus_renderers_enabled.push(dioxus_feature_enabled.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is exactly one renderer enabled by this feature, we can use it
|
||||
if let [feature_name] = dioxus_renderers_enabled.as_slice() {
|
||||
if feature_name == dioxus_feature {
|
||||
tracing::debug!(
|
||||
"Found feature {key} for renderer {renderer} which enables dioxus/{renderer}"
|
||||
);
|
||||
return Some(key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
});
|
||||
|
||||
res.or_else(|| {
|
||||
let depends_on_dioxus = package.dependencies.iter().any(|dep| dep.name == "dioxus");
|
||||
if depends_on_dioxus {
|
||||
let fallback = format!("dioxus/{dioxus_feature}");
|
||||
tracing::debug!(
|
||||
"Could not find explicit feature for renderer {renderer}, passing `fallback` instead"
|
||||
);
|
||||
Some(fallback)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+812
-4898
File diff suppressed because it is too large.
Load diff
@@ -1,359 +0,0 @@
|
||||
use crate::Result;
|
||||
use anyhow::Context;
|
||||
use itertools::Itertools;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use target_lexicon::{
|
||||
Aarch64Architecture, Architecture, ArmArchitecture, Triple, X86_32Architecture,
|
||||
};
|
||||
use tokio::process::Command;
|
||||
|
||||
/// The tools for Android (ndk, sdk, etc)
|
||||
///
|
||||
/// <https://gist.github.com/Pulimet/5013acf2cd5b28e55036c82c91bd56d8?permalink_comment_id=3678614>
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AndroidTools {
|
||||
pub(crate) sdk: Option<PathBuf>,
|
||||
pub(crate) ndk: PathBuf,
|
||||
pub(crate) adb: PathBuf,
|
||||
pub(crate) java_home: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn get_android_tools() -> Option<Arc<AndroidTools>> {
|
||||
// We check for SDK first since users might install Android Studio and then install the SDK
|
||||
// After that they might install the NDK, so the SDK drives the source of truth.
|
||||
let sdk = var_or_debug("ANDROID_SDK_ROOT")
|
||||
.or_else(|| var_or_debug("ANDROID_SDK"))
|
||||
.or_else(|| var_or_debug("ANDROID_HOME"));
|
||||
|
||||
// Check the ndk. We look for users's overrides first and then look into the SDK.
|
||||
// Sometimes users set only the NDK (especially if they're somewhat advanced) so we need to look for it manually
|
||||
//
|
||||
// Might look like this, typically under "sdk":
|
||||
// "/Users/jonkelley/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang"
|
||||
let ndk = var_or_debug("NDK_HOME")
|
||||
.or_else(|| var_or_debug("ANDROID_NDK_HOME"))
|
||||
.or_else(|| {
|
||||
// Look for the most recent NDK in the event the user has installed multiple NDK
|
||||
// Eventually we might need to drive this from Dioxus.toml
|
||||
let sdk = sdk.as_ref()?;
|
||||
let ndk_dir = sdk.join("ndk").read_dir().ok()?;
|
||||
ndk_dir
|
||||
.flatten()
|
||||
.map(|dir| (dir.file_name(), dir.path()))
|
||||
.sorted()
|
||||
.next_back()
|
||||
.map(|(_, path)| path.to_path_buf())
|
||||
})?;
|
||||
|
||||
// Look for ADB in the SDK. If it's not there we'll use `adb` from the PATH
|
||||
let adb = sdk
|
||||
.as_ref()
|
||||
.and_then(|sdk| {
|
||||
let tools = sdk.join("platform-tools");
|
||||
if tools.join("adb").exists() {
|
||||
return Some(tools.join("adb"));
|
||||
}
|
||||
if tools.join("adb.exe").exists() {
|
||||
return Some(tools.join("adb.exe"));
|
||||
}
|
||||
None
|
||||
})
|
||||
.unwrap_or_else(|| PathBuf::from("adb"));
|
||||
|
||||
// https://stackoverflow.com/questions/71381050/java-home-is-set-to-an-invalid-directory-android-studio-flutter
|
||||
// always respect the user's JAVA_HOME env var above all other options
|
||||
//
|
||||
// we only attempt autodetection if java_home is not set
|
||||
//
|
||||
// this is a better fallback than falling onto the users' system java home since many users might
|
||||
// not even know which java that is - they just know they have android studio installed
|
||||
let java_home = std::env::var_os("JAVA_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| {
|
||||
// Attempt to autodetect java home from the android studio path or jdk path on macos
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let jbr_home =
|
||||
PathBuf::from("/Applications/Android Studio.app/Contents/jbr/Contents/Home/");
|
||||
if jbr_home.exists() {
|
||||
return Some(jbr_home);
|
||||
}
|
||||
|
||||
let jre_home =
|
||||
PathBuf::from("/Applications/Android Studio.app/Contents/jre/Contents/Home");
|
||||
if jre_home.exists() {
|
||||
return Some(jre_home);
|
||||
}
|
||||
|
||||
let jdk_home =
|
||||
PathBuf::from("/Library/Java/JavaVirtualMachines/openjdk.jdk/Contents/Home/");
|
||||
if jdk_home.exists() {
|
||||
return Some(jdk_home);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let jbr_home = PathBuf::from("C:\\Program Files\\Android\\Android Studio\\jbr");
|
||||
if jbr_home.exists() {
|
||||
return Some(jbr_home);
|
||||
}
|
||||
}
|
||||
|
||||
// todo(jon): how do we detect java home on linux?
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let jbr_home = PathBuf::from("/usr/lib/jvm/java-11-openjdk-amd64");
|
||||
if jbr_home.exists() {
|
||||
return Some(jbr_home);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
});
|
||||
|
||||
Some(Arc::new(AndroidTools {
|
||||
ndk,
|
||||
adb,
|
||||
java_home,
|
||||
sdk,
|
||||
}))
|
||||
}
|
||||
|
||||
impl AndroidTools {
|
||||
pub(crate) fn android_tools_dir(&self) -> PathBuf {
|
||||
let prebuilt = self.ndk.join("toolchains").join("llvm").join("prebuilt");
|
||||
|
||||
if cfg!(target_os = "macos") {
|
||||
// for whatever reason, even on aarch64 macos, the linker is under darwin-x86_64
|
||||
return prebuilt.join("darwin-x86_64").join("bin");
|
||||
}
|
||||
|
||||
if cfg!(target_os = "linux") {
|
||||
return prebuilt.join("linux-x86_64").join("bin");
|
||||
}
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
return prebuilt.join("windows-x86_64").join("bin");
|
||||
}
|
||||
|
||||
// Otherwise return the first entry in the prebuilt directory
|
||||
prebuilt
|
||||
.read_dir()
|
||||
.expect("Failed to read android toolchains directory")
|
||||
.next()
|
||||
.expect("Failed to find android toolchains directory")
|
||||
.expect("Failed to read android toolchain file")
|
||||
.path()
|
||||
}
|
||||
|
||||
/// Return the location of the clang toolchain for the given target triple.
|
||||
///
|
||||
/// Note that we use clang:
|
||||
/// "~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang"
|
||||
///
|
||||
/// But if we needed the linker, we would use:
|
||||
/// "~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/ld"
|
||||
///
|
||||
/// However, for our purposes, we only go through the cc driver and not the linker directly.
|
||||
pub(crate) fn android_cc(&self, triple: &Triple, sdk_version: u32) -> PathBuf {
|
||||
let suffix = if cfg!(target_os = "windows") {
|
||||
".cmd"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let target = match triple.architecture {
|
||||
Architecture::Arm(_) => "armv7a-linux-androideabi",
|
||||
_ => &triple.to_string(),
|
||||
};
|
||||
|
||||
self.android_tools_dir()
|
||||
.join(format!("{}{}-clang{}", target, sdk_version, suffix))
|
||||
}
|
||||
|
||||
pub(crate) fn sysroot(&self) -> PathBuf {
|
||||
// The sysroot is usually located in the NDK under:
|
||||
// "~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/sysroot"
|
||||
// or similar, depending on the platform.
|
||||
self.android_tools_dir().parent().unwrap().join("sysroot")
|
||||
}
|
||||
|
||||
pub(crate) fn sdk(&self) -> PathBuf {
|
||||
// /Users/jonathankelley/Library/Android/sdk/ndk/25.2/... (25.2 is the ndk here)
|
||||
// /Users/jonathankelley/Library/Android/sdk/
|
||||
self.sdk
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.ndk.parent().unwrap().parent().unwrap().to_path_buf())
|
||||
}
|
||||
|
||||
pub(crate) fn emulator(&self) -> PathBuf {
|
||||
self.sdk().join("emulator").join("emulator")
|
||||
}
|
||||
|
||||
pub(crate) fn clang_folder(&self) -> PathBuf {
|
||||
// The clang folder is usually located in the NDK under:
|
||||
// "~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/lib/clang/<version>"
|
||||
// or similar, depending on the platform.
|
||||
self.android_tools_dir()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("lib")
|
||||
.join("clang")
|
||||
}
|
||||
|
||||
pub(crate) fn ranlib(&self) -> PathBuf {
|
||||
self.android_tools_dir().join("llvm-ranlib")
|
||||
}
|
||||
|
||||
pub(crate) fn ar_path(&self) -> PathBuf {
|
||||
self.android_tools_dir().join("llvm-ar")
|
||||
}
|
||||
|
||||
pub(crate) fn target_cc(&self) -> PathBuf {
|
||||
self.android_tools_dir().join("clang")
|
||||
}
|
||||
|
||||
pub(crate) fn target_cxx(&self) -> PathBuf {
|
||||
self.android_tools_dir().join("clang++")
|
||||
}
|
||||
|
||||
pub(crate) fn java_home(&self) -> Option<PathBuf> {
|
||||
self.java_home.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn android_jnilib(triple: &Triple) -> &'static str {
|
||||
use target_lexicon::Architecture;
|
||||
match triple.architecture {
|
||||
Architecture::Arm(_) => "armeabi-v7a",
|
||||
Architecture::Aarch64(_) => "arm64-v8a",
|
||||
Architecture::X86_32(_) => "x86",
|
||||
Architecture::X86_64 => "x86_64",
|
||||
_ => unimplemented!("Unsupported architecture"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn autodetect_android_device_triple(&self) -> Triple {
|
||||
// Use the host's triple and then convert field by field
|
||||
// ie, the "best" emulator for an m1 mac would be: "aarch64-linux-android"
|
||||
// - We assume android is always "linux"
|
||||
// - We try to match the architecture unless otherwise specified. This is because
|
||||
// emulators that match the host arch are usually faster.
|
||||
let mut triple = "aarch64-linux-android".parse::<Triple>().unwrap();
|
||||
|
||||
// TODO: Wire this up with --device flag. (add `-s serial`` flag before `shell` arg)
|
||||
let output = Command::new(&self.adb)
|
||||
.arg("shell")
|
||||
.arg("uname")
|
||||
.arg("-m")
|
||||
.output()
|
||||
.await
|
||||
.map(|out| String::from_utf8(out.stdout));
|
||||
|
||||
match output {
|
||||
Ok(Ok(out)) => match out.trim() {
|
||||
"armv7l" | "armv8l" => {
|
||||
triple.architecture = Architecture::Arm(ArmArchitecture::Arm)
|
||||
}
|
||||
"aarch64" => {
|
||||
triple.architecture = Architecture::Aarch64(Aarch64Architecture::Aarch64)
|
||||
}
|
||||
"i386" => triple.architecture = Architecture::X86_32(X86_32Architecture::I386),
|
||||
"x86_64" => {
|
||||
triple.architecture = Architecture::X86_64;
|
||||
}
|
||||
"" => {
|
||||
tracing::debug!("No device running - probably waiting for emulator");
|
||||
}
|
||||
other => {
|
||||
tracing::debug!("Unknown architecture from adb: {other}");
|
||||
}
|
||||
},
|
||||
Ok(Err(err)) => {
|
||||
tracing::debug!("Failed to parse adb output: {err}");
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::debug!("ADB command failed: {:?}", err);
|
||||
}
|
||||
};
|
||||
|
||||
triple
|
||||
}
|
||||
|
||||
pub(crate) fn libcpp_shared(&self, triple: &Triple) -> PathBuf {
|
||||
// The libc++_shared.so is usually located in the sysroot under:
|
||||
// "~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/<arch>/libc++_shared.so"
|
||||
// or similar, depending on the platform.
|
||||
self.sysroot()
|
||||
.join("usr")
|
||||
.join("lib")
|
||||
.join(Self::sysroot_target(&triple.to_string()))
|
||||
.join("libc++_shared.so")
|
||||
}
|
||||
|
||||
pub(crate) fn sysroot_target(rust_target: &str) -> &str {
|
||||
(match rust_target {
|
||||
"armv7-linux-androideabi" => "arm-linux-androideabi",
|
||||
_ => rust_target,
|
||||
}) as _
|
||||
}
|
||||
|
||||
pub(crate) fn openssl_prebuilt_aar() -> &'static [u8] {
|
||||
include_bytes!("../../assets/android/prebuilt/openssl-1.1.1q-beta-1.tar.gz")
|
||||
}
|
||||
|
||||
pub(crate) fn openssl_prebuilt_dest() -> PathBuf {
|
||||
crate::Workspace::dioxus_data_dir()
|
||||
.join("prebuilt")
|
||||
.join("openssl-1.1.1q-beta-1")
|
||||
}
|
||||
|
||||
pub(crate) fn openssl_lib_dir(arch: &Triple) -> PathBuf {
|
||||
let libs_dir = Self::openssl_prebuilt_dest().join("ssl").join("libs");
|
||||
|
||||
match arch.architecture {
|
||||
Architecture::Arm(_) => libs_dir.join("android.armeabi-v7a"),
|
||||
Architecture::Aarch64(_) => libs_dir.join("android.arm64-v8a"),
|
||||
Architecture::X86_32(_) => libs_dir.join("android.x86"),
|
||||
Architecture::X86_64 => libs_dir.join("android.x86_64"),
|
||||
_ => libs_dir.join("android.arm64-v8a"), // Default to arm64-v8a
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn openssl_include_dir() -> PathBuf {
|
||||
Self::openssl_prebuilt_dest().join("ssl").join("include")
|
||||
}
|
||||
|
||||
/// Unzip the prebuilt OpenSSL AAR file into the `.dx/prebuilt/openssl-<version>` directory
|
||||
pub(crate) fn unpack_prebuilt_openssl() -> Result<()> {
|
||||
let raw_aar = AndroidTools::openssl_prebuilt_aar();
|
||||
let aar_dest = AndroidTools::openssl_prebuilt_dest();
|
||||
|
||||
if aar_dest.exists() {
|
||||
tracing::trace!("Prebuilt OpenSSL already exists at {:?}", aar_dest);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
std::fs::create_dir_all(aar_dest.parent().context("no parent for aar")?)
|
||||
.context("failed to create prebuilt OpenSSL directory")?;
|
||||
|
||||
// Unpack the entire tar.gz file into the destination directory
|
||||
let mut archive = tar::Archive::new(flate2::read::GzDecoder::new(raw_aar as &[u8]));
|
||||
archive
|
||||
.unpack(aar_dest.parent().context("no parent for aar dest")?)
|
||||
.context("failed to unpack prebuilt OpenSSL archive")?;
|
||||
|
||||
tracing::debug!("Unpacked prebuilt OpenSSL to {:?}", aar_dest);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn var_or_debug(name: &str) -> Option<PathBuf> {
|
||||
use std::env::var;
|
||||
|
||||
var(name)
|
||||
.inspect_err(|_| tracing::trace!("{name} not set"))
|
||||
.ok()
|
||||
.map(PathBuf::from)
|
||||
}
|
||||
@@ -0,0 +1,603 @@
|
||||
//! ### Web:
|
||||
//!
|
||||
//! Create a folder that is somewhat similar to an app-image (exe + asset)
|
||||
//! The server is dropped into the `web` folder, even if there's no `public` folder.
|
||||
//! If there's no server (SPA), we still use the `web` folder, but it only contains the
|
||||
//! public folder.
|
||||
//!
|
||||
//! ```
|
||||
//! web/
|
||||
//! server
|
||||
//! assets/
|
||||
//! public/
|
||||
//! index.html
|
||||
//! wasm/
|
||||
//! app.wasm
|
||||
//! glue.js
|
||||
//! snippets/
|
||||
//! ...
|
||||
//! assets/
|
||||
//! logo.png
|
||||
//! ```
|
||||
//!
|
||||
//! ### Linux:
|
||||
//!
|
||||
//! <https://docs.appimage.org/reference/appdir.html#ref-appdir>
|
||||
//! current_exe.join("Assets")
|
||||
//! ```
|
||||
//! app.appimage/
|
||||
//! AppRun
|
||||
//! app.desktop
|
||||
//! package.json
|
||||
//! assets/
|
||||
//! logo.png
|
||||
//! ```
|
||||
|
||||
use crate::{opt::AppManifest, BuildMode, BuildRequest};
|
||||
use crate::{BuildContext, BundleFormat, Result, TraceSrc, WasmBindgen, WasmOptConfig};
|
||||
use anyhow::Context;
|
||||
use dioxus_cli_config::format_base_path_meta_element;
|
||||
use manganis::AssetOptions;
|
||||
use manganis_core::AssetVariant;
|
||||
use std::{
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
impl BuildRequest {
|
||||
pub async fn verify_web_tooling(&self) -> Result<()> {
|
||||
// Wasm bindgen
|
||||
let krate_bindgen_version =
|
||||
self.workspace
|
||||
.wasm_bindgen_version()
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"failed to detect wasm-bindgen version, unable to proceed"
|
||||
))?;
|
||||
|
||||
WasmBindgen::verify_install(&krate_bindgen_version).await?;
|
||||
|
||||
// esbuild is used for JS asset processing
|
||||
let _esbuild_path = crate::esbuild::Esbuild::get_or_install().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bundle the web app
|
||||
/// - Run wasm-bindgen
|
||||
/// - Bundle split
|
||||
/// - Run wasm-opt
|
||||
/// - Register the .wasm and .js files with the asset system
|
||||
pub async fn bundle_web(
|
||||
&self,
|
||||
ctx: &BuildContext,
|
||||
exe: &Path,
|
||||
assets: &mut AppManifest,
|
||||
) -> Result<()> {
|
||||
use crate::{wasm_bindgen::WasmBindgen, wasm_opt};
|
||||
use std::fmt::Write;
|
||||
|
||||
// Locate the output of the build files and the bindgen output
|
||||
// We'll fill these in a second if they don't already exist
|
||||
let bindgen_outdir = self.wasm_bindgen_out_dir();
|
||||
let post_bindgen_wasm = self.wasm_bindgen_wasm_output_file();
|
||||
let should_bundle_split: bool = self.wasm_split;
|
||||
let bindgen_version = self
|
||||
.workspace
|
||||
.wasm_bindgen_version()
|
||||
.expect("this should have been checked by tool verification");
|
||||
|
||||
// Prepare any work dirs
|
||||
_ = std::fs::remove_dir_all(&bindgen_outdir);
|
||||
std::fs::create_dir_all(&bindgen_outdir)?;
|
||||
|
||||
// Lift the internal functions to exports
|
||||
if ctx.mode == BuildMode::Fat {
|
||||
let unprocessed = std::fs::read(exe)?;
|
||||
let all_exported_bytes = crate::build::prepare_wasm_base_module(&unprocessed)?;
|
||||
std::fs::write(exe, all_exported_bytes)?;
|
||||
}
|
||||
|
||||
// Prepare our configuration
|
||||
//
|
||||
// we turn on debug symbols in dev mode
|
||||
//
|
||||
// We leave demangling to false since it's faster and these tools seem to prefer the raw symbols.
|
||||
// todo(jon): investigate if the chrome extension needs them demangled or demangles them automatically.
|
||||
let keep_debug = self.config.web.wasm_opt.debug
|
||||
|| self.debug_symbols
|
||||
|| self.wasm_split
|
||||
|| !self.release
|
||||
|| ctx.mode == BuildMode::Fat;
|
||||
let keep_names = self.config.web.wasm_opt.keep_names
|
||||
|| self.keep_names
|
||||
|| self.wasm_split
|
||||
|| ctx.mode == BuildMode::Fat;
|
||||
let demangle = false;
|
||||
let wasm_opt_options = WasmOptConfig {
|
||||
memory_packing: self.wasm_split,
|
||||
debug: self.debug_symbols,
|
||||
..self.config.web.wasm_opt.clone()
|
||||
};
|
||||
|
||||
// Run wasm-bindgen. Some of the options are not "optimal" but will be fixed up by wasm-opt
|
||||
//
|
||||
// There's performance implications here. Running with --debug is slower than without
|
||||
// We're keeping around lld sections and names but wasm-opt will fix them
|
||||
// todo(jon): investigate a good balance of wiping debug symbols during dev (or doing a double build?)
|
||||
ctx.status_wasm_bindgen_start();
|
||||
tracing::debug!(dx_src = ?TraceSrc::Bundle, "Running wasm-bindgen");
|
||||
let start = std::time::Instant::now();
|
||||
WasmBindgen::new(&bindgen_version)
|
||||
.input_path(exe)
|
||||
.target("web")
|
||||
.debug(keep_debug)
|
||||
.demangle(demangle)
|
||||
.keep_debug(keep_debug)
|
||||
.keep_lld_exports(true)
|
||||
.out_name(self.executable_name())
|
||||
.out_dir(&bindgen_outdir)
|
||||
.remove_name_section(!keep_names)
|
||||
.remove_producers_section(!keep_names)
|
||||
.run()
|
||||
.await
|
||||
.context("Failed to generate wasm-bindgen bindings")?;
|
||||
tracing::debug!(dx_src = ?TraceSrc::Bundle, "wasm-bindgen complete in {:?}", start.elapsed());
|
||||
|
||||
// Run bundle splitting if the user has requested it
|
||||
// It's pretty expensive but because of rayon should be running separate threads, hopefully
|
||||
// not blocking this thread. Dunno if that's true
|
||||
if should_bundle_split {
|
||||
ctx.status_splitting_bundle();
|
||||
|
||||
// Load the contents of these binaries since we need both of them
|
||||
// We're going to use the default makeLoad glue from wasm-split
|
||||
let original = std::fs::read(exe)?;
|
||||
let bindgened = std::fs::read(&post_bindgen_wasm)?;
|
||||
let mut glue = wasm_split_cli::MAKE_LOAD_JS.to_string();
|
||||
|
||||
// Run the emitter
|
||||
let splitter = wasm_split_cli::Splitter::new(&original, &bindgened);
|
||||
let modules = splitter
|
||||
.context("Failed to parse wasm for splitter")?
|
||||
.emit()
|
||||
.context("Failed to emit wasm split modules")?;
|
||||
|
||||
// Write the chunks that contain shared imports
|
||||
// These will be in the format of chunk_0_modulename.wasm - this is hardcoded in wasm-split
|
||||
tracing::debug!("Writing split chunks to disk");
|
||||
for (idx, chunk) in modules.chunks.iter().enumerate() {
|
||||
let path = bindgen_outdir.join(format!("chunk_{}_{}.wasm", idx, chunk.module_name));
|
||||
wasm_opt::write_wasm(&chunk.bytes, &path, &wasm_opt_options).await?;
|
||||
writeln!(
|
||||
glue, "export const __wasm_split_load_chunk_{idx} = makeLoad(\"/{base_path}/assets/{url}\", [], fusedImports);",
|
||||
base_path = self.base_path_or_default(),
|
||||
url = assets
|
||||
.register_asset(&path, AssetOptions::builder().into_asset_options())?.bundled_path(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// Write the modules that contain the entrypoints
|
||||
tracing::debug!("Writing split modules to disk");
|
||||
for (idx, module) in modules.modules.iter().enumerate() {
|
||||
let comp_name = module
|
||||
.component_name
|
||||
.as_ref()
|
||||
.context("generated bindgen module has no name?")?;
|
||||
|
||||
let path = bindgen_outdir.join(format!("module_{idx}_{comp_name}.wasm"));
|
||||
wasm_opt::write_wasm(&module.bytes, &path, &wasm_opt_options).await?;
|
||||
|
||||
let hash_id = module
|
||||
.hash_id
|
||||
.as_ref()
|
||||
.context("generated wasm-split bindgen module has no hash id?")?;
|
||||
|
||||
writeln!(
|
||||
glue,
|
||||
"export const __wasm_split_load_{module}_{hash_id}_{comp_name} = makeLoad(\"/{base_path}/assets/{url}\", [{deps}], fusedImports);",
|
||||
module = module.module_name,
|
||||
|
||||
base_path = self.base_path_or_default(),
|
||||
|
||||
// Again, register this wasm with the asset system
|
||||
url = assets
|
||||
.register_asset(&path, AssetOptions::builder().into_asset_options())?
|
||||
.bundled_path(),
|
||||
|
||||
// This time, make sure to write the dependencies of this chunk
|
||||
// The names here are again, hardcoded in wasm-split - fix this eventually.
|
||||
deps = module
|
||||
.relies_on_chunks
|
||||
.iter()
|
||||
.map(|idx| format!("__wasm_split_load_chunk_{idx}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)?;
|
||||
}
|
||||
|
||||
// Write the js binding
|
||||
// It's not registered as an asset since it will get included in the main.js file
|
||||
let js_output_path = bindgen_outdir.join("__wasm_split.js");
|
||||
std::fs::write(&js_output_path, &glue)?;
|
||||
|
||||
// Make sure to write some entropy to the main.js file so it gets a new hash
|
||||
// If we don't do this, the main.js file will be cached and never pick up the chunk names
|
||||
let uuid = Uuid::new_v5(&Uuid::NAMESPACE_URL, glue.as_bytes());
|
||||
std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(self.wasm_bindgen_js_output_file())
|
||||
.context("Failed to open main.js file")?
|
||||
.write_all(format!("/*{uuid}*/").as_bytes())?;
|
||||
|
||||
// Write the main wasm_bindgen file and register it with the asset system
|
||||
// This will overwrite the file in place
|
||||
// We will wasm-opt it in just a second...
|
||||
std::fs::write(&post_bindgen_wasm, modules.main.bytes).unwrap();
|
||||
}
|
||||
|
||||
if matches!(ctx.mode, BuildMode::Fat) {
|
||||
// add `export { __wbg_get_imports };` to the end of the wasmbindgen js file
|
||||
let mut js = std::fs::read(self.wasm_bindgen_js_output_file())?;
|
||||
writeln!(js, "\nexport {{ __wbg_get_imports }};")?;
|
||||
std::fs::write(self.wasm_bindgen_js_output_file(), js)?;
|
||||
}
|
||||
|
||||
// Make sure to optimize the main wasm file if requested or if bundle splitting
|
||||
if should_bundle_split || self.release {
|
||||
ctx.status_optimizing_wasm();
|
||||
wasm_opt::optimize(&post_bindgen_wasm, &post_bindgen_wasm, &wasm_opt_options).await?;
|
||||
}
|
||||
|
||||
if self.should_bundle_to_asset() {
|
||||
// Make sure to register the main wasm file with the asset system
|
||||
assets.register_asset(
|
||||
&post_bindgen_wasm,
|
||||
AssetOptions::builder().into_asset_options(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// Now that the wasm is registered as an asset, we can write the js glue shim
|
||||
self.write_js_glue_shim(assets)?;
|
||||
|
||||
if self.should_bundle_to_asset() {
|
||||
// Register the main.js with the asset system so it bundles in the snippets and optimizes
|
||||
assets.register_asset(
|
||||
&self.wasm_bindgen_js_output_file(),
|
||||
AssetOptions::js()
|
||||
.with_minify(true)
|
||||
.with_preload(true)
|
||||
.into_asset_options(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// Write the index.html file with the pre-configured contents we got from pre-rendering
|
||||
self.write_index_html(assets)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_js_glue_shim(&self, assets: &AppManifest) -> Result<()> {
|
||||
let wasm_path = self.bundled_wasm_path(assets);
|
||||
|
||||
// Load and initialize wasm without requiring a separate javascript file.
|
||||
// This also allows using a strict Content-Security-Policy.
|
||||
let mut js = std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(self.wasm_bindgen_js_output_file())?;
|
||||
let mut buf_writer = std::io::BufWriter::new(&mut js);
|
||||
writeln!(
|
||||
buf_writer,
|
||||
r#"
|
||||
globalThis.__wasm_split_main_initSync = initSync;
|
||||
|
||||
// Actually perform the load
|
||||
__wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
||||
// assign this module to be accessible globally
|
||||
globalThis.__dx_mainWasm = wasm;
|
||||
globalThis.__dx_mainInit = __wbg_init;
|
||||
globalThis.__dx_mainInitSync = initSync;
|
||||
globalThis.__dx___wbg_get_imports = __wbg_get_imports;
|
||||
|
||||
if (wasm.__wbindgen_start == undefined) {{
|
||||
wasm.main();
|
||||
}}
|
||||
}});
|
||||
"#,
|
||||
self.base_path_or_default(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write the index.html file to the output directory. This must be called after the wasm and js
|
||||
/// assets are registered with the asset system if this is a release build.
|
||||
pub(crate) fn write_index_html(&self, assets: &AppManifest) -> Result<()> {
|
||||
let wasm_path = self.bundled_wasm_path(assets);
|
||||
let js_path = self.bundled_js_path(assets);
|
||||
|
||||
// Write the index.html file with the pre-configured contents we got from pre-rendering
|
||||
std::fs::write(
|
||||
self.root_dir().join("index.html"),
|
||||
self.prepare_html(assets, &wasm_path, &js_path).unwrap(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bundled_js_path(&self, assets: &AppManifest) -> String {
|
||||
let wasm_bindgen_js_out = self.wasm_bindgen_js_output_file();
|
||||
if self.should_bundle_to_asset() {
|
||||
let name = assets
|
||||
.get_first_asset_for_source(&wasm_bindgen_js_out)
|
||||
.expect("The js source must exist before creating index.html");
|
||||
format!("assets/{}", name.bundled_path())
|
||||
} else {
|
||||
format!(
|
||||
"wasm/{}",
|
||||
wasm_bindgen_js_out.file_name().unwrap().to_str().unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to the wasm-bindgen output files. Either the direct file or the optimized one depending on the build mode
|
||||
fn bundled_wasm_path(&self, assets: &AppManifest) -> String {
|
||||
let wasm_bindgen_wasm_out = self.wasm_bindgen_wasm_output_file();
|
||||
if self.should_bundle_to_asset() {
|
||||
let name = assets
|
||||
.get_first_asset_for_source(&wasm_bindgen_wasm_out)
|
||||
.expect("The wasm source must exist before creating index.html");
|
||||
format!("assets/{}", name.bundled_path())
|
||||
} else {
|
||||
format!(
|
||||
"wasm/{}",
|
||||
wasm_bindgen_wasm_out.file_name().unwrap().to_str().unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Users create an index.html for their SPA if they want it
|
||||
///
|
||||
/// We always write our wasm as main.js and main_bg.wasm
|
||||
///
|
||||
/// In prod we run the optimizer which bundles everything together properly
|
||||
///
|
||||
/// So their index.html needs to include main.js in the scripts otherwise nothing happens?
|
||||
///
|
||||
/// Seems like every platform has a weird file that declares a bunch of stuff
|
||||
/// - web: index.html
|
||||
/// - ios: info.plist
|
||||
/// - macos: info.plist
|
||||
/// - linux: appimage root thing?
|
||||
/// - android: androidmanifest.xml
|
||||
///
|
||||
/// You also might different variants of these files (staging / prod) and different flavors (eu/us)
|
||||
///
|
||||
/// web's index.html is weird since it's not just a bundle format but also a *content* format
|
||||
pub(crate) fn prepare_html(
|
||||
&self,
|
||||
assets: &AppManifest,
|
||||
wasm_path: &str,
|
||||
js_path: &str,
|
||||
) -> Result<String> {
|
||||
let mut html = {
|
||||
const DEV_DEFAULT_HTML: &str = include_str!("../../assets/web/dev.index.html");
|
||||
const PROD_DEFAULT_HTML: &str = include_str!("../../assets/web/prod.index.html");
|
||||
|
||||
let crate_root: &Path = &self.crate_dir();
|
||||
let custom_html_file = crate_root.join("index.html");
|
||||
let default_html = match self.release {
|
||||
true => PROD_DEFAULT_HTML,
|
||||
false => DEV_DEFAULT_HTML,
|
||||
};
|
||||
std::fs::read_to_string(custom_html_file).unwrap_or_else(|_| String::from(default_html))
|
||||
};
|
||||
|
||||
// Inject any resources from the config into the html
|
||||
self.inject_resources(assets, &mut html)?;
|
||||
|
||||
// Inject loading scripts if they are not already present
|
||||
self.inject_loading_scripts(assets, &mut html);
|
||||
|
||||
// Replace any special placeholders in the HTML with resolved values
|
||||
self.replace_template_placeholders(&mut html, wasm_path, js_path);
|
||||
|
||||
let title = self.config.web.app.title.clone();
|
||||
Self::replace_or_insert_before("{app_title}", "</title", &title, &mut html);
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
fn is_dev_build(&self) -> bool {
|
||||
!self.release
|
||||
}
|
||||
|
||||
// Inject any resources from the config into the html
|
||||
fn inject_resources(&self, assets: &AppManifest, html: &mut String) -> Result<()> {
|
||||
use std::fmt::Write;
|
||||
|
||||
// Collect all resources into a list of styles and scripts
|
||||
let resources = &self.config.web.resource;
|
||||
let mut style_list = resources.style.clone().unwrap_or_default();
|
||||
let mut script_list = resources.script.clone().unwrap_or_default();
|
||||
|
||||
if self.is_dev_build() {
|
||||
style_list.extend(resources.dev.style.iter().cloned());
|
||||
script_list.extend(resources.dev.script.iter().cloned());
|
||||
}
|
||||
|
||||
let mut head_resources = String::new();
|
||||
|
||||
// Add all styles to the head
|
||||
for style in &style_list {
|
||||
writeln!(
|
||||
&mut head_resources,
|
||||
"<link rel=\"stylesheet\" href=\"{}\">",
|
||||
&style.to_str().unwrap(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// Add all scripts to the head
|
||||
for script in &script_list {
|
||||
writeln!(
|
||||
&mut head_resources,
|
||||
"<script src=\"{}\"></script>",
|
||||
&script.to_str().unwrap(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// Add the base path to the head if this is a debug build
|
||||
if self.is_dev_build() {
|
||||
if let Some(base_path) = &self.trimmed_base_path() {
|
||||
head_resources.push_str(&format_base_path_meta_element(base_path));
|
||||
}
|
||||
}
|
||||
|
||||
// Inject any resources from manganis into the head
|
||||
for asset in assets.unique_assets() {
|
||||
let asset_path = asset.bundled_path();
|
||||
match asset.options().variant() {
|
||||
AssetVariant::Css(css_options) => {
|
||||
if css_options.preloaded() {
|
||||
_ = write!(
|
||||
head_resources,
|
||||
r#"<link rel="preload" as="style" href="/{{base_path}}/assets/{asset_path}" crossorigin>"#
|
||||
);
|
||||
}
|
||||
if css_options.static_head() {
|
||||
_ = write!(
|
||||
head_resources,
|
||||
r#"<link rel="stylesheet" href="/{{base_path}}/assets/{asset_path}" type="text/css">"#
|
||||
);
|
||||
}
|
||||
}
|
||||
AssetVariant::Image(image_options) => {
|
||||
if image_options.preloaded() {
|
||||
_ = write!(
|
||||
head_resources,
|
||||
r#"<link rel="preload" as="image" href="/{{base_path}}/assets/{asset_path}" crossorigin>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
AssetVariant::Js(js_options) => {
|
||||
if js_options.preloaded() {
|
||||
_ = write!(
|
||||
head_resources,
|
||||
r#"<link rel="preload" as="script" href="/{{base_path}}/assets/{asset_path}" crossorigin>"#
|
||||
);
|
||||
}
|
||||
if js_options.static_head() {
|
||||
_ = write!(
|
||||
head_resources,
|
||||
r#"<script src="/{{base_path}}/assets/{asset_path}"></script>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Do not preload the wasm file, because in Safari, preload as=fetch requires additional fetch() options to exactly match the network request
|
||||
// And if they do not match then Safari downloads the wasm file twice.
|
||||
// See https://github.com/wasm-bindgen/wasm-bindgen/blob/ac51055a4c39fa0affe02f7b63fb1d4c9b3ddfaf/crates/cli-support/src/js/mod.rs#L967
|
||||
Self::replace_or_insert_before("{style_include}", "</head", &head_resources, html);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inject loading scripts if they are not already present
|
||||
fn inject_loading_scripts(&self, assets: &AppManifest, html: &mut String) {
|
||||
// If the current build opted out of injecting loading scripts, don't inject anything
|
||||
if !self.inject_loading_scripts {
|
||||
return;
|
||||
}
|
||||
|
||||
// If not, insert the script
|
||||
*html = html.replace(
|
||||
"</body",
|
||||
&format!(
|
||||
r#"<script type="module" async src="/{}/{}"></script>
|
||||
</body"#,
|
||||
self.base_path_or_default(),
|
||||
self.bundled_js_path(assets)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Replace any special placeholders in the HTML with resolved values
|
||||
fn replace_template_placeholders(&self, html: &mut String, wasm_path: &str, js_path: &str) {
|
||||
let base_path = self.base_path_or_default();
|
||||
*html = html.replace("{base_path}", base_path);
|
||||
|
||||
let app_name = &self.executable_name();
|
||||
|
||||
// If the html contains the old `{app_name}` placeholder, replace {app_name}_bg.wasm and {app_name}.js
|
||||
// with the new paths
|
||||
*html = html.replace("wasm/{app_name}_bg.wasm", wasm_path);
|
||||
*html = html.replace("wasm/{app_name}.js", js_path);
|
||||
|
||||
// Otherwise replace the new placeholders
|
||||
*html = html.replace("{wasm_path}", wasm_path);
|
||||
*html = html.replace("{js_path}", js_path);
|
||||
|
||||
// Replace the app_name if we find it anywhere standalone
|
||||
*html = html.replace("{app_name}", app_name);
|
||||
}
|
||||
|
||||
/// Replace a string or insert the new contents before a marker
|
||||
fn replace_or_insert_before(
|
||||
replace: &str,
|
||||
or_insert_before: &str,
|
||||
with: &str,
|
||||
content: &mut String,
|
||||
) {
|
||||
if content.contains(replace) {
|
||||
*content = content.replace(replace, with);
|
||||
} else if let Some(pos) = content.find(or_insert_before) {
|
||||
content.insert_str(pos, with);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if assets should be pre_compressed. This will only be true in release mode if the user
|
||||
/// has enabled pre_compress in the web config.
|
||||
pub fn should_pre_compress_web_assets(&self, release: bool) -> bool {
|
||||
self.config.web.pre_compress & release
|
||||
}
|
||||
|
||||
/// Check if the wasm output should be bundled to an asset type app.
|
||||
pub(crate) fn should_bundle_to_asset(&self) -> bool {
|
||||
self.release && self.bundle == BundleFormat::Web
|
||||
}
|
||||
|
||||
/// Get the path to the wasm bindgen temporary output folder
|
||||
pub fn wasm_bindgen_out_dir(&self) -> PathBuf {
|
||||
self.root_dir().join("wasm")
|
||||
}
|
||||
|
||||
/// Get the path to the wasm bindgen javascript output file
|
||||
pub(crate) fn wasm_bindgen_js_output_file(&self) -> PathBuf {
|
||||
self.wasm_bindgen_out_dir()
|
||||
.join(self.executable_name())
|
||||
.with_extension("js")
|
||||
}
|
||||
|
||||
/// Get the path to the wasm bindgen wasm output file
|
||||
pub(crate) fn wasm_bindgen_wasm_output_file(&self) -> PathBuf {
|
||||
self.wasm_bindgen_out_dir()
|
||||
.join(format!("{}_bg", self.executable_name()))
|
||||
.with_extension("wasm")
|
||||
}
|
||||
|
||||
pub(crate) fn path_is_in_public_dir(&self, path: &Path) -> bool {
|
||||
let Some(static_dir) = self.user_public_dir() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Canonicalize when possible so we work with editors that use tmp files
|
||||
let canonical_static =
|
||||
dunce::canonicalize(&static_dir).unwrap_or_else(|_| static_dir.clone());
|
||||
let canonical_path = dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
|
||||
|
||||
canonical_path.starts_with(&canonical_static)
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ impl<'a> BundleContext<'a> {
|
||||
// Build the resources map from assets + config resources
|
||||
let mut resources_map = HashMap::new();
|
||||
|
||||
let asset_dir = build.asset_dir();
|
||||
let asset_dir = build.bundle_asset_dir();
|
||||
if asset_dir.exists() {
|
||||
for entry in walkdir::WalkDir::new(&asset_dir) {
|
||||
let entry = entry?;
|
||||
|
||||
@@ -198,7 +198,7 @@ impl CommandWithPlatformOverrides<BuildArgs> {
|
||||
|
||||
// Run SSG and cache static routes
|
||||
if ssg {
|
||||
crate::pre_render_static_routes(None, &mut server_build, None).await?;
|
||||
server_build.pre_render_static_routes(None, None).await?;
|
||||
}
|
||||
|
||||
tracing::info!(path = ?server.root_dir(), "Server build completed successfully! 🚀");
|
||||
@@ -213,20 +213,20 @@ impl BuildArtifacts {
|
||||
// The tip crate is identified by replacing hyphens with underscores in the target name,
|
||||
// but since we don't have the BuildRequest here, we look for the entry with link_args
|
||||
// (only the tip crate has link_args attached) or fall back to any entry.
|
||||
let tip_args = self
|
||||
.workspace_rustc_args
|
||||
.values()
|
||||
.find(|a| !a.link_args.is_empty())
|
||||
.or_else(|| self.workspace_rustc_args.values().next())
|
||||
.cloned()
|
||||
let (rustc_args, rustc_envs) = self
|
||||
.workspace_rustc
|
||||
.rustc_args
|
||||
.iter()
|
||||
.find(|(k, _v)| k.ends_with(".bin"))
|
||||
.map(|f| (f.1.args.clone(), f.1.envs.clone()))
|
||||
.unwrap_or_default();
|
||||
|
||||
StructuredBuildArtifacts {
|
||||
path: self.root_dir,
|
||||
exe: self.exe,
|
||||
rustc_args: tip_args.args,
|
||||
rustc_envs: tip_args.envs,
|
||||
link_args: tip_args.link_args,
|
||||
rustc_args,
|
||||
rustc_envs,
|
||||
link_args: self.workspace_rustc.link_args,
|
||||
assets: self.assets.unique_assets().cloned().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{fs::create_dir_all, path::PathBuf};
|
||||
|
||||
use crate::opt::process_file_to;
|
||||
use crate::{extract_assets_from_file, Result, StructuredOutput};
|
||||
use crate::{Result, StructuredOutput};
|
||||
use clap::Parser;
|
||||
use tracing::debug;
|
||||
|
||||
@@ -16,9 +16,10 @@ pub struct BuildAssets {
|
||||
|
||||
impl BuildAssets {
|
||||
pub async fn run(self) -> Result<StructuredOutput> {
|
||||
let manifest = extract_assets_from_file(&self.executable).await?;
|
||||
let manifest = crate::extract_symbols_from_file(&self.executable).await?;
|
||||
|
||||
create_dir_all(&self.destination)?;
|
||||
|
||||
for asset in manifest.unique_assets() {
|
||||
let source_path = PathBuf::from(asset.absolute_source_path());
|
||||
let destination_path = self.destination.join(asset.bundled_path());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::{Result, Workspace};
|
||||
use crate::{AndroidTools, Result, Workspace};
|
||||
use anyhow::{bail, Context};
|
||||
use itertools::Itertools;
|
||||
|
||||
@@ -91,24 +91,24 @@ impl Doctor {
|
||||
let mut sdk = "not found".to_string();
|
||||
let mut java_home = "not found".to_string();
|
||||
let mut emulator = "not found".to_string();
|
||||
if let Some(rf) = crate::build::get_android_tools() {
|
||||
if rf.adb.exists() {
|
||||
adb = rf.adb.display().to_string();
|
||||
if let Some(tools) = AndroidTools::current() {
|
||||
if tools.adb.exists() {
|
||||
adb = tools.adb.display().to_string();
|
||||
}
|
||||
if rf.ndk.exists() {
|
||||
ndk = rf.ndk.display().to_string();
|
||||
if tools.ndk.exists() {
|
||||
ndk = tools.ndk.display().to_string();
|
||||
}
|
||||
if let Some(jh) = rf.java_home.as_ref() {
|
||||
if let Some(jh) = tools.java_home.as_ref() {
|
||||
java_home = jh.display().to_string();
|
||||
}
|
||||
if rf.sdk().exists() {
|
||||
sdk = rf.sdk().display().to_string();
|
||||
if tools.sdk().exists() {
|
||||
sdk = tools.sdk().display().to_string();
|
||||
}
|
||||
if let Some(jh) = rf.java_home.as_ref() {
|
||||
if let Some(jh) = tools.java_home.as_ref() {
|
||||
java_home = jh.display().to_string();
|
||||
}
|
||||
if rf.emulator().exists() {
|
||||
emulator = rf.emulator().display().to_string();
|
||||
if tools.emulator().exists() {
|
||||
emulator = tools.emulator().display().to_string();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::{
|
||||
platform_override::CommandWithPlatformOverrides, AppBuilder, BuildArgs, BuildId, BuildMode,
|
||||
HotpatchModuleCache, Result, StructuredOutput,
|
||||
HotpatchModuleCache, Result, StructuredOutput, WorkspaceRustcArgs,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use dioxus_dx_wire_format::StructuredBuildArtifacts;
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, io::Read};
|
||||
|
||||
const HELP_HEADING: &str = "Hotpatching a binary";
|
||||
|
||||
@@ -71,24 +71,21 @@ impl HotpatchTip {
|
||||
let cache = Arc::new(HotpatchModuleCache::new(&exe, &request.triple)?);
|
||||
|
||||
let tip_crate_name = request.main_target.replace('-', "_");
|
||||
let mut workspace_rustc_args = HashMap::new();
|
||||
workspace_rustc_args.insert(
|
||||
let mut workspace_rustc_args = WorkspaceRustcArgs::new(link_args);
|
||||
workspace_rustc_args.rustc_args.insert(
|
||||
format!("{tip_crate_name}.bin"),
|
||||
crate::RustcArgs {
|
||||
args: rustc_args,
|
||||
envs: rustc_envs,
|
||||
link_args,
|
||||
},
|
||||
);
|
||||
|
||||
let mode = BuildMode::Thin {
|
||||
workspace_rustc_args,
|
||||
changed_files: vec![],
|
||||
changed_crates: vec![],
|
||||
modified_crates: std::collections::HashSet::new(),
|
||||
aslr_reference: self.aslr_reference,
|
||||
cache: cache.clone(),
|
||||
object_cache: crate::ObjectCache::new(&request.session_cache_dir()),
|
||||
};
|
||||
|
||||
let artifacts = AppBuilder::started(request, mode, build_id)?
|
||||
|
||||
@@ -80,6 +80,8 @@ impl LinkAction {
|
||||
})
|
||||
}
|
||||
|
||||
/// Write the required set of environment variables such that the child process has
|
||||
/// enough information to perform the linking itself.
|
||||
pub(crate) fn write_env_vars(
|
||||
&self,
|
||||
env_vars: &mut Vec<(Cow<'static, str>, OsString)>,
|
||||
|
||||
@@ -161,6 +161,7 @@ impl RunArgs {
|
||||
BuilderUpdate::ProcessWaitFailed { err } => {
|
||||
return Err(err.into());
|
||||
}
|
||||
BuilderUpdate::ProfilePhase { .. } => {}
|
||||
}
|
||||
}
|
||||
ServeUpdate::Exit { .. } => break,
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::config::{
|
||||
pub struct AndroidPermissionEntry {
|
||||
/// Full Android permission string (e.g., "android.permission.CAMERA")
|
||||
pub permission: String,
|
||||
|
||||
/// User-facing description (used for documentation)
|
||||
pub description: String,
|
||||
}
|
||||
@@ -24,6 +25,7 @@ pub struct AndroidPermissionEntry {
|
||||
pub struct PlistEntry {
|
||||
/// Plist key (e.g., "NSCameraUsageDescription")
|
||||
pub key: String,
|
||||
|
||||
/// User-facing description shown in permission dialogs
|
||||
pub value: String,
|
||||
}
|
||||
@@ -4,6 +4,7 @@ mod component;
|
||||
mod dioxus_config;
|
||||
mod inline_config;
|
||||
mod manifest;
|
||||
mod manifest_mapper;
|
||||
mod serve;
|
||||
mod web;
|
||||
|
||||
@@ -12,5 +13,6 @@ pub(crate) use bundle::*;
|
||||
pub(crate) use dioxus_config::*;
|
||||
pub(crate) use inline_config::*;
|
||||
pub(crate) use manifest::*;
|
||||
pub(crate) use manifest_mapper::*;
|
||||
pub(crate) use serve::*;
|
||||
pub(crate) use web::*;
|
||||
@@ -1,4 +1,5 @@
|
||||
use manganis_core::BundledAsset;
|
||||
use manganis::SwiftPackageMetadata;
|
||||
use manganis_core::{AndroidArtifactMetadata, BundledAsset};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -14,16 +15,33 @@ mod json;
|
||||
pub(crate) use file::process_file_to;
|
||||
pub(crate) use hash::add_hash_to_asset;
|
||||
|
||||
/// A manifest of all assets collected from dependencies
|
||||
/// A manifest of all assets collected from dependencies. This is persisted to disk for users to be
|
||||
/// able to pick up the result of asset extraction.
|
||||
///
|
||||
/// This will be filled in primarily by incremental compilation artifacts.
|
||||
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct AssetManifest {
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct AppManifest {
|
||||
/// Stable since 0.7.0
|
||||
pub cli_version: String,
|
||||
|
||||
/// Map of bundled asset name to the asset itself
|
||||
assets: BTreeMap<PathBuf, HashSet<BundledAsset>>,
|
||||
pub assets: BTreeMap<PathBuf, HashSet<BundledAsset>>,
|
||||
|
||||
pub android_artifacts: Vec<AndroidArtifactMetadata>,
|
||||
|
||||
pub swift_sources: Vec<SwiftPackageMetadata>,
|
||||
}
|
||||
|
||||
impl AssetManifest {
|
||||
impl AppManifest {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cli_version: crate::VERSION.to_string(),
|
||||
android_artifacts: Default::default(),
|
||||
swift_sources: Default::default(),
|
||||
assets: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Manually add an asset to the manifest
|
||||
pub fn register_asset(
|
||||
&mut self,
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::{args, vars},
|
||||
path::PathBuf,
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
/// A "capture" of a workspace's rustc commands, cumulated by reading the various rustc commands
|
||||
/// from disk.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct WorkspaceRustcArgs {
|
||||
pub link_args: Vec<String>,
|
||||
pub rustc_args: HashMap<String, RustcArgs>,
|
||||
}
|
||||
|
||||
impl WorkspaceRustcArgs {
|
||||
pub fn new(link_args: Vec<String>) -> Self {
|
||||
Self {
|
||||
link_args,
|
||||
rustc_args: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RustcArgs {
|
||||
pub args: Vec<String>,
|
||||
pub envs: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// The environment variable indicating where the args directory is located.
|
||||
///
|
||||
/// When `dx-rustc` runs, it writes each workspace crate's arguments to a
|
||||
@@ -23,12 +47,92 @@ pub fn is_wrapping_rustc() -> bool {
|
||||
std::env::var(DX_RUSTC_WRAPPER_ENV_VAR).is_ok()
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RustcArgs {
|
||||
pub args: Vec<String>,
|
||||
pub envs: Vec<(String, String)>,
|
||||
/// it doesn't include first program name argument
|
||||
pub link_args: Vec<String>,
|
||||
/// Run rustc directly, but output the result to a per-crate file in the args directory.
|
||||
///
|
||||
/// <https://doc.rust-lang.org/cargo/reference/config.html#buildrustc>
|
||||
pub fn run_rustc() -> ExitCode {
|
||||
let args_dir: PathBuf = std::env::var(DX_RUSTC_WRAPPER_ENV_VAR)
|
||||
.expect("DX_RUSTC env var must be set")
|
||||
.into();
|
||||
|
||||
// Cargo invokes a workspace wrapper like: `wrapper-name rustc [args...]`
|
||||
// We skip our own executable name (`wrapper-name`) to get the args passed to us.
|
||||
let captured_args = args().skip(1).collect::<Vec<_>>();
|
||||
|
||||
let rustc_args = RustcArgs {
|
||||
args: captured_args.clone(),
|
||||
envs: vars().collect::<_>(),
|
||||
};
|
||||
|
||||
// Always persist the captured rustc invocation, even for link steps.
|
||||
// The tip crate's bin target is typically only observed during the final link invocation,
|
||||
// so returning early before writing would lose the exact args/envs we need for fat-link replay.
|
||||
write_rustc_args(&args_dir, &rustc_args);
|
||||
|
||||
// If we are being asked to link, delegate to the linker action after capturing.
|
||||
if has_linking_args() {
|
||||
return crate::link::LinkAction::from_env()
|
||||
.expect("Linker action not found")
|
||||
.run_link();
|
||||
}
|
||||
|
||||
// Run the actual rustc command.
|
||||
// We want all stdout/stderr to be inherited, so the user sees the compiler output.
|
||||
let mut cmd = std::process::Command::new("rustc");
|
||||
|
||||
// The first argument in `captured_args` is the rustc path, which we need to skip
|
||||
// when passing arguments to the `rustc` command we are spawning.
|
||||
cmd.args(captured_args.iter().skip(1));
|
||||
cmd.envs(rustc_args.envs);
|
||||
cmd.stdout(std::process::Stdio::inherit());
|
||||
cmd.stderr(std::process::Stdio::inherit());
|
||||
cmd.current_dir(std::env::current_dir().expect("Failed to get current dir"));
|
||||
|
||||
// Spawn the process and propagate its exit code.
|
||||
let status = cmd.status().expect("Failed to execute rustc command");
|
||||
std::process::exit(status.code().unwrap_or(1)); // Exit with 1 if process was killed by signal
|
||||
}
|
||||
|
||||
fn write_rustc_args(args_dir: &PathBuf, rustc_args: &RustcArgs) {
|
||||
// Extract the crate name from the args to use as the filename.
|
||||
// Skip non-sensical args when a build is completely fresh (rustc is invoked with --crate-name ___)
|
||||
let crate_name = rustc_args
|
||||
.args
|
||||
.iter()
|
||||
.skip_while(|arg| *arg != "--crate-name")
|
||||
.nth(1);
|
||||
|
||||
if let Some(crate_name) = crate_name {
|
||||
if crate_name != "___" {
|
||||
std::fs::create_dir_all(args_dir)
|
||||
.expect("Failed to create args directory for rustc wrapper");
|
||||
|
||||
let crate_type = rustc_args
|
||||
.args
|
||||
.iter()
|
||||
.skip_while(|arg| *arg != "--crate-type")
|
||||
.nth(1)
|
||||
.map(|s| s.as_str());
|
||||
|
||||
let serialized_args =
|
||||
serde_json::to_string(rustc_args).expect("Failed to serialize rustc args");
|
||||
|
||||
// Write args with an explicit target suffix: {crate_name}.lib.json or
|
||||
// {crate_name}.bin.json. This avoids the ambiguity of a bare {crate_name}.json
|
||||
// and ensures lib+bin crates don't overwrite each other.
|
||||
let suffix = match crate_type {
|
||||
Some("lib" | "rlib") => "lib",
|
||||
Some("bin") => "bin",
|
||||
_ => "bin", // proc-macro, cdylib, etc. — treat as bin
|
||||
};
|
||||
|
||||
std::fs::write(
|
||||
args_dir.join(format!("{crate_name}.{suffix}.json")),
|
||||
&serialized_args,
|
||||
)
|
||||
.expect("Failed to write rustc args to file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the arguments indicate a linking step, including those in command files.
|
||||
@@ -64,85 +168,3 @@ fn has_linking_args() -> bool {
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Run rustc directly, but output the result to a per-crate file in the args directory.
|
||||
///
|
||||
/// <https://doc.rust-lang.org/cargo/reference/config.html#buildrustc>
|
||||
pub fn run_rustc() -> ExitCode {
|
||||
// If we are being asked to link, delegate to the linker action.
|
||||
if has_linking_args() {
|
||||
return crate::link::LinkAction::from_env()
|
||||
.expect("Linker action not found")
|
||||
.run_link();
|
||||
}
|
||||
|
||||
let args_dir: PathBuf = std::env::var(DX_RUSTC_WRAPPER_ENV_VAR)
|
||||
.expect("DX_RUSTC env var must be set")
|
||||
.into();
|
||||
|
||||
// Cargo invokes a workspace wrapper like: `wrapper-name rustc [args...]`
|
||||
// We skip our own executable name (`wrapper-name`) to get the args passed to us.
|
||||
let captured_args = args().skip(1).collect::<Vec<_>>();
|
||||
|
||||
let rustc_args = RustcArgs {
|
||||
args: captured_args.clone(),
|
||||
envs: vars().collect::<_>(),
|
||||
link_args: Default::default(),
|
||||
};
|
||||
|
||||
// Extract the crate name from the args to use as the filename.
|
||||
// Skip non-sensical args when a build is completely fresh (rustc is invoked with --crate-name ___)
|
||||
let crate_name = rustc_args
|
||||
.args
|
||||
.iter()
|
||||
.skip_while(|arg| *arg != "--crate-name")
|
||||
.nth(1);
|
||||
|
||||
if let Some(crate_name) = crate_name {
|
||||
if crate_name != "___" {
|
||||
std::fs::create_dir_all(&args_dir)
|
||||
.expect("Failed to create args directory for rustc wrapper");
|
||||
|
||||
let crate_type = rustc_args
|
||||
.args
|
||||
.iter()
|
||||
.skip_while(|arg| *arg != "--crate-type")
|
||||
.nth(1)
|
||||
.map(|s| s.as_str());
|
||||
|
||||
let serialized_args =
|
||||
serde_json::to_string(&rustc_args).expect("Failed to serialize rustc args");
|
||||
|
||||
// Write args with an explicit target suffix: {crate_name}.lib.json or
|
||||
// {crate_name}.bin.json. This avoids the ambiguity of a bare {crate_name}.json
|
||||
// and ensures lib+bin crates don't overwrite each other.
|
||||
let suffix = match crate_type {
|
||||
Some("lib" | "rlib") => "lib",
|
||||
Some("bin") => "bin",
|
||||
_ => "bin", // proc-macro, cdylib, etc. — treat as bin
|
||||
};
|
||||
|
||||
std::fs::write(
|
||||
args_dir.join(format!("{crate_name}.{suffix}.json")),
|
||||
&serialized_args,
|
||||
)
|
||||
.expect("Failed to write rustc args to file");
|
||||
}
|
||||
}
|
||||
|
||||
// Run the actual rustc command.
|
||||
// We want all stdout/stderr to be inherited, so the user sees the compiler output.
|
||||
let mut cmd = std::process::Command::new("rustc");
|
||||
|
||||
// The first argument in `captured_args` is the rustc path, which we need to skip
|
||||
// when passing arguments to the `rustc` command we are spawning.
|
||||
cmd.args(captured_args.iter().skip(1));
|
||||
cmd.envs(rustc_args.envs);
|
||||
cmd.stdout(std::process::Stdio::inherit());
|
||||
cmd.stderr(std::process::Stdio::inherit());
|
||||
cmd.current_dir(std::env::current_dir().expect("Failed to get current dir"));
|
||||
|
||||
// Spawn the process and propagate its exit code.
|
||||
let status = cmd.status().expect("Failed to execute rustc command");
|
||||
std::process::exit(status.code().unwrap_or(1)); // Exit with 1 if process was killed by signal
|
||||
}
|
||||
@@ -159,6 +159,7 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &TraceController) -> Resu
|
||||
}
|
||||
}
|
||||
BuilderUpdate::Progress { .. } => {}
|
||||
BuilderUpdate::ProfilePhase { .. } => {}
|
||||
BuilderUpdate::CompilerMessage { message } => {
|
||||
screen.push_cargo_log(message);
|
||||
}
|
||||
@@ -248,24 +249,23 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &TraceController) -> Resu
|
||||
screen.push_log(log);
|
||||
}
|
||||
|
||||
ServeUpdate::OpenApp => match builder.use_hotpatch_engine {
|
||||
true if !matches!(builder.client.build.bundle, BundleFormat::Web) => {
|
||||
ServeUpdate::OpenApp => {
|
||||
if builder.use_hotpatch_engine
|
||||
&& !matches!(builder.client.build.bundle, BundleFormat::Web)
|
||||
{
|
||||
tracing::warn!(
|
||||
"Opening a native app with hotpatching enabled requires a full rebuild..."
|
||||
);
|
||||
builder.full_rebuild().await;
|
||||
devserver.send_reload_start().await;
|
||||
devserver.start_build().await;
|
||||
} else if let Err(err) = builder.open_all(&devserver, true).await {
|
||||
tracing::error!(
|
||||
"Failed to open app: {}",
|
||||
crate::error::log_stacktrace(&err, 15)
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
if let Err(err) = builder.open_all(&devserver, true).await {
|
||||
tracing::error!(
|
||||
"Failed to open app: {}",
|
||||
crate::error::log_stacktrace(&err, 15)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
ServeUpdate::Redraw => {
|
||||
// simply returning will cause a redraw
|
||||
|
||||
@@ -259,12 +259,12 @@ impl AppServer {
|
||||
if !self.ssg || server.stage != BuildStage::Success {
|
||||
return;
|
||||
}
|
||||
if let Err(err) = crate::pre_render_static_routes(
|
||||
Some(devserver.devserver_address()),
|
||||
server,
|
||||
Some(&server.tx.clone()),
|
||||
)
|
||||
.await
|
||||
if let Err(err) = server
|
||||
.pre_render_static_routes(
|
||||
Some(devserver.devserver_address()),
|
||||
Some(&server.tx.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to pre-render static routes: {err}");
|
||||
}
|
||||
@@ -362,7 +362,7 @@ impl AppServer {
|
||||
// This prevents losing changes from tools like stylance, tailwind, or sass that generate files
|
||||
// in response to source changes.
|
||||
tracing::debug!(
|
||||
"Queueing file change: client is not ready to receive hotreloads. Files: {:#?}",
|
||||
"Queueing file change - client is not ready to receive hotreloads. Files: {:?}",
|
||||
files
|
||||
);
|
||||
self.pending_file_changes.extend(files.iter().cloned());
|
||||
@@ -410,7 +410,6 @@ impl AppServer {
|
||||
// Get the cached file if it exists - ignoring if it doesn't exist
|
||||
let Some(cached_file) = self.file_map.get_mut(path) else {
|
||||
tracing::debug!("No entry for file in filemap: {:?}", path);
|
||||
tracing::debug!("Filemap: {:#?}", self.file_map.keys());
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -601,10 +600,12 @@ impl AppServer {
|
||||
use crate::cli::styles::GLOW_STYLE;
|
||||
|
||||
if should_open {
|
||||
let time_taken = artifacts
|
||||
.time_end
|
||||
.duration_since(artifacts.time_start)
|
||||
.unwrap();
|
||||
let time_taken = self.client.total_build_time().unwrap_or_else(|| {
|
||||
artifacts
|
||||
.time_end
|
||||
.duration_since(artifacts.time_start)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
if self.client.builds_opened == 0 {
|
||||
tracing::info!(
|
||||
|
||||
@@ -265,6 +265,7 @@ impl WebServer {
|
||||
BuilderUpdate::StderrReceived { .. } => {}
|
||||
BuilderUpdate::ProcessExited { .. } => {}
|
||||
BuilderUpdate::ProcessWaitFailed { .. } => {}
|
||||
BuilderUpdate::ProfilePhase { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ async fn test_harnesses() {
|
||||
assert_eq!(targets.client.triple, "aarch64-apple-ios".parse().unwrap());
|
||||
})
|
||||
.asrt(r#"dx build --android --device"#, |targets| async move {
|
||||
if crate::get_android_tools().is_none() {
|
||||
if crate::AndroidTools::current().is_none() {
|
||||
// Skip when Android NDK/SDK is not installed
|
||||
return;
|
||||
}
|
||||
@@ -254,7 +254,6 @@ async fn test_harnesses() {
|
||||
assert!(t.server.is_none());
|
||||
assert_eq!(t.client.bundle, BundleFormat::Ios);
|
||||
assert_eq!(t.client.triple, TestHarnessBuilder::host_ios_triple_sim());
|
||||
assert!(t.client.no_default_features);
|
||||
},
|
||||
),
|
||||
TestHarnessBuilder::new("harness-fullstack-with-optional-tokio")
|
||||
|
||||
@@ -91,9 +91,9 @@ impl WasmBindgen {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn keep_lld_sections(self, keep_lld_sections: bool) -> Self {
|
||||
pub(crate) fn keep_lld_exports(self, keep_lld_exports: bool) -> Self {
|
||||
Self {
|
||||
keep_lld_exports: keep_lld_sections,
|
||||
keep_lld_exports,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use tar::Archive;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
/// Pinned binaryen version (contains wasm-opt).
|
||||
const BINARYEN_VERSION: &str = "127";
|
||||
const BINARYEN_VERSION: &str = "129";
|
||||
|
||||
/// Write these wasm bytes with a particular set of optimizations
|
||||
pub async fn write_wasm(bytes: &[u8], output_path: &Path, cfg: &WasmOptConfig) -> Result<()> {
|
||||
|
||||
@@ -101,7 +101,7 @@ impl Workspace {
|
||||
)
|
||||
.context("Failed to load Cargo.toml")?;
|
||||
|
||||
let android_tools = crate::build::get_android_tools();
|
||||
let android_tools = AndroidTools::current();
|
||||
|
||||
let workspace = Arc::new(Self {
|
||||
krates,
|
||||
|
||||
Reference in new issue
Block a user