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
@@ -38,7 +38,6 @@ node_modules/
|
|||||||
|
|
||||||
# ignore the output of tmps
|
# ignore the output of tmps
|
||||||
tmp/
|
tmp/
|
||||||
bundle/
|
|
||||||
|
|
||||||
# in debugging we frequently dump wasm to wat with `wasm-tools print`
|
# in debugging we frequently dump wasm to wat with `wasm-tools print`
|
||||||
*.wat
|
*.wat
|
||||||
+19
-23
@@ -6,9 +6,25 @@ fn main() {
|
|||||||
println!("cargo:rerun-if-changed=.git/HEAD");
|
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||||
println!("cargo:rerun-if-changed=.git/refs");
|
println!("cargo:rerun-if-changed=.git/refs");
|
||||||
|
|
||||||
let full_hash = std::env::var("DIOXUS_CLI_GIT_SHA")
|
let full_hash = std::env::var("DIOXUS_CLI_GIT_SHA").ok().or_else(|| {
|
||||||
.ok()
|
let output = Command::new("git")
|
||||||
.or_else(resolve_git_commit_hash);
|
.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 {
|
if let Some(full_hash) = full_hash {
|
||||||
println!("cargo:rustc-env=DIOXUS_CLI_GIT_SHA={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}");
|
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.
|
//! build system.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
fs::OpenOptions,
|
||||||
io::{Cursor, Read, Seek, Write},
|
io::{Cursor, Read, Seek, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::opt::AssetManifest;
|
use crate::opt::AppManifest;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use const_serialize::{deserialize_const, serialize_const, ConstVec};
|
use const_serialize::{deserialize_const, serialize_const, ConstVec};
|
||||||
use manganis::{AssetOptions, AssetVariant, BundledAsset, ImageFormat, ImageSize};
|
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 object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol};
|
||||||
use pdb::FallibleIterator;
|
use pdb::FallibleIterator;
|
||||||
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
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)]
|
#[derive(Copy, Clone)]
|
||||||
enum ManganisVersion {
|
enum ManganisVersion {
|
||||||
/// The legacy version of the manganis format published with 0.7.0 and 0.7.1
|
/// The legacy version of the manganis format published with 0.7.0 and 0.7.1
|
||||||
Legacy,
|
Legacy,
|
||||||
|
|
||||||
/// The new version of the manganis format 0.7.2 onward
|
/// The new version of the manganis format 0.7.2 onward
|
||||||
/// This now includes both assets (old BundledAsset format) and permissions (SymbolData format)
|
/// This now includes both assets (old BundledAsset format) and permissions (SymbolData format)
|
||||||
New,
|
New,
|
||||||
@@ -696,37 +686,25 @@ fn find_wasm_symbol_offsets<'a, R: ReadRef<'a>>(
|
|||||||
Ok(offsets)
|
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.
|
/// 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.
|
/// Also extracts Android/Swift plugin metadata for FFI bindings.
|
||||||
pub(crate) async fn extract_symbols_from_file(
|
pub(crate) async fn extract_symbols_from_file(path: impl AsRef<Path>) -> Result<AppManifest> {
|
||||||
path: impl AsRef<Path>,
|
|
||||||
) -> Result<SymbolExtractionResult> {
|
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let mut file = open_file_for_writing_with_timeout(
|
let mut file =
|
||||||
path,
|
open_file_for_writing_with_timeout(path, OpenOptions::new().write(true).read(true)).await?;
|
||||||
std::fs::OpenOptions::new().write(true).read(true),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut file_contents = Vec::new();
|
let mut file_contents = Vec::new();
|
||||||
file.read_to_end(&mut file_contents)?;
|
file.read_to_end(&mut file_contents)?;
|
||||||
let mut reader = Cursor::new(&file_contents);
|
|
||||||
let read_cache = ReadCache::new(&mut reader);
|
let (offsets, obj_format) = {
|
||||||
let object_file = object::File::parse(&read_cache)?;
|
let mut reader = Cursor::new(&file_contents);
|
||||||
let offsets = find_symbol_offsets(path, &file_contents, &object_file)?;
|
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 assets = Vec::new();
|
||||||
let mut android_artifacts = Vec::new();
|
let mut android_artifacts = Vec::new();
|
||||||
@@ -824,7 +802,11 @@ pub(crate) async fn extract_symbols_from_file(
|
|||||||
|
|
||||||
match entry.representation {
|
match entry.representation {
|
||||||
AssetRepresentation::RawBundled => {
|
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);
|
let new_data = version.serialize_asset(&asset);
|
||||||
if new_data.len() > version.size() {
|
if new_data.len() > version.size() {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -861,14 +843,15 @@ pub(crate) async fn extract_symbols_from_file(
|
|||||||
.context("Failed to sync file after writing assets")?;
|
.context("Failed to sync file after writing assets")?;
|
||||||
|
|
||||||
// If the file is a macos binary, we need to re-sign the modified binary
|
// 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
|
// 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("--force")
|
||||||
.arg("--sign")
|
.arg("--sign")
|
||||||
.arg("-") // Sign with an empty identity
|
.arg("-") // Sign with an empty identity
|
||||||
.arg(path)
|
.arg(path)
|
||||||
.output()
|
.output()
|
||||||
|
.await
|
||||||
.context("Failed to run codesign - is `codesign` in your path?")?;
|
.context("Failed to run codesign - is `codesign` in your path?")?;
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -878,23 +861,15 @@ pub(crate) async fn extract_symbols_from_file(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SymbolExtractionResult {
|
let mut manifest = AppManifest::new();
|
||||||
assets,
|
|
||||||
android_artifacts,
|
|
||||||
swift_packages,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find all assets in the given file, hash them, and write them back to the file.
|
for asset in assets {
|
||||||
/// 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 {
|
|
||||||
manifest.insert_asset(asset);
|
manifest.insert_asset(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
manifest.android_artifacts = android_artifacts;
|
||||||
|
manifest.swift_sources = swift_packages;
|
||||||
|
|
||||||
Ok(manifest)
|
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.
|
/// 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(
|
async fn open_file_for_writing_with_timeout(
|
||||||
file: &Path,
|
file: &Path,
|
||||||
options: &mut std::fs::OpenOptions,
|
options: &mut OpenOptions,
|
||||||
) -> Result<std::fs::File> {
|
) -> Result<std::fs::File> {
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = Instant::now();
|
||||||
let timeout = std::time::Duration::from_secs(5);
|
let timeout = Duration::from_secs(5);
|
||||||
loop {
|
loop {
|
||||||
match options.open(file) {
|
match options.open(file) {
|
||||||
Ok(file) => return Ok(file),
|
Ok(file) => return Ok(file),
|
||||||
@@ -916,7 +891,7 @@ async fn open_file_for_writing_with_timeout(
|
|||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
"Failed to open file because another process is using it. Retrying..."
|
"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 {
|
} else {
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
@@ -946,3 +921,16 @@ fn write_serialized_bytes(
|
|||||||
|
|
||||||
Ok(())
|
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::{
|
use crate::{
|
||||||
build::cache::ObjectCache, serve::WebServer, verbosity_or_default, BuildArtifacts,
|
serve::WebServer, verbosity_or_default, BuildArtifacts, BuildRequest, BuildStage,
|
||||||
BuildRequest, BuildStage, BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result,
|
BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result, StructuredOutput,
|
||||||
RustcArgs, StructuredOutput,
|
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Context, Error};
|
use anyhow::{bail, Context, Error};
|
||||||
use futures_util::{future::OptionFuture, pin_mut, FutureExt};
|
use futures_util::{future::OptionFuture, pin_mut, FutureExt};
|
||||||
@@ -10,7 +9,7 @@ use itertools::Itertools;
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
env,
|
env,
|
||||||
time::{Duration, Instant, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
@@ -98,10 +97,10 @@ pub(crate) struct AppBuilder {
|
|||||||
pub compiled_crates: usize,
|
pub compiled_crates: usize,
|
||||||
pub expected_crates: usize,
|
pub expected_crates: usize,
|
||||||
pub bundling_progress: f64,
|
pub bundling_progress: f64,
|
||||||
pub compile_start: Option<Instant>,
|
pub compile_start: Option<SystemTime>,
|
||||||
pub compile_end: Option<Instant>,
|
pub compile_end: Option<SystemTime>,
|
||||||
pub bundle_start: Option<Instant>,
|
pub bundle_start: Option<SystemTime>,
|
||||||
pub bundle_end: Option<Instant>,
|
pub bundle_end: Option<SystemTime>,
|
||||||
|
|
||||||
/// The debugger for the app - must be enabled with the `d` key
|
/// The debugger for the app - must be enabled with the `d` key
|
||||||
pub(crate) pid: Option<u32>,
|
pub(crate) pid: Option<u32>,
|
||||||
@@ -110,8 +109,8 @@ pub(crate) struct AppBuilder {
|
|||||||
/// Each patch includes objects from ALL crates in this set.
|
/// Each patch includes objects from ALL crates in this set.
|
||||||
pub modified_crates: HashSet<String>,
|
pub modified_crates: HashSet<String>,
|
||||||
|
|
||||||
/// Cache of the latest `.rcgu.o` files for each modified workspace crate.
|
/// The build profiling spans for us to generate a flamegraph from.
|
||||||
pub object_cache: ObjectCache,
|
pub profile_spans: Vec<BuildPhaseProfile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppBuilder {
|
impl AppBuilder {
|
||||||
@@ -153,7 +152,7 @@ impl AppBuilder {
|
|||||||
expected_crates: 1,
|
expected_crates: 1,
|
||||||
bundling_progress: 0.0,
|
bundling_progress: 0.0,
|
||||||
builds_opened: 0,
|
builds_opened: 0,
|
||||||
compile_start: Some(Instant::now()),
|
compile_start: Some(SystemTime::now()),
|
||||||
aslr_reference: None,
|
aslr_reference: None,
|
||||||
compile_end: None,
|
compile_end: None,
|
||||||
bundle_start: None,
|
bundle_start: None,
|
||||||
@@ -169,7 +168,7 @@ impl AppBuilder {
|
|||||||
patch_cache: None,
|
patch_cache: None,
|
||||||
pid: None,
|
pid: None,
|
||||||
modified_crates: HashSet::new(),
|
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) {
|
pub(crate) fn start(&mut self, mode: BuildMode, build_id: BuildId) {
|
||||||
self.build_task = tokio::spawn({
|
let request = self.build.clone();
|
||||||
let request = self.build.clone();
|
let tx = self.tx.clone();
|
||||||
let tx = self.tx.clone();
|
self.build_task = tokio::spawn(async move {
|
||||||
async move {
|
let ctx = BuildContext::new(tx, mode, build_id);
|
||||||
let ctx = BuildContext {
|
request.verify_tooling(&ctx).await?;
|
||||||
mode,
|
request.prebuild(&ctx).await?;
|
||||||
build_id,
|
request.build(ctx).await
|
||||||
tx: tx.clone(),
|
|
||||||
};
|
|
||||||
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
|
// 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
|
// todo - move this handling to a separate function that won't be cancelled
|
||||||
match &update {
|
match &update {
|
||||||
BuilderUpdate::Progress { stage } => {
|
BuilderUpdate::Progress { .. } if self.is_finished() => {
|
||||||
// Prevent updates from flowing in after the build has already 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 {
|
match stage {
|
||||||
BuildStage::Initializing => {
|
BuildStage::Initializing => {
|
||||||
self.compiled_crates = 0;
|
self.profile_spans.clear();
|
||||||
self.bundling_progress = 0.0;
|
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 => {}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
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 { ref bundle } => {
|
||||||
BuilderUpdate::BuildReady { .. } => {
|
// 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.compiled_crates = self.expected_crates;
|
||||||
self.bundling_progress = 1.0;
|
self.bundling_progress = 1.0;
|
||||||
self.stage = BuildStage::Success;
|
self.stage = BuildStage::Success;
|
||||||
|
|
||||||
self.complete_compile();
|
self.bundle_end = Some(SystemTime::now());
|
||||||
self.bundle_end = Some(Instant::now());
|
if self.compile_end.is_none() {
|
||||||
|
self.compiled_crates = self.expected_crates;
|
||||||
|
self.compile_end = Some(SystemTime::now());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BuilderUpdate::BuildFailed { .. } => {
|
BuilderUpdate::BuildFailed { .. } => {
|
||||||
tracing::debug!("Setting builder to failed state");
|
tracing::debug!("Setting builder to failed state");
|
||||||
self.stage = BuildStage::Failed;
|
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 { .. } => {}
|
StdoutReceived { .. } => {}
|
||||||
StderrReceived { .. } => {}
|
StderrReceived { .. } => {}
|
||||||
ProcessExited { .. } => {}
|
ProcessExited { .. } => {}
|
||||||
@@ -332,6 +344,23 @@ impl AppBuilder {
|
|||||||
update
|
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(
|
pub(crate) fn patch_rebuild(
|
||||||
&mut self,
|
&mut self,
|
||||||
changed_files: Vec<PathBuf>,
|
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
|
// Abort all the ongoing builds, cleaning up any loose artifacts and waiting to cleanly exit
|
||||||
self.abort_all(BuildStage::Restarting);
|
self.abort_all(BuildStage::Restarting);
|
||||||
|
self.compile_start = Some(SystemTime::now());
|
||||||
|
self.profile_spans.clear();
|
||||||
self.build_task = tokio::spawn({
|
self.build_task = tokio::spawn({
|
||||||
let request = self.build.clone();
|
let request = self.build.clone();
|
||||||
let ctx = BuildContext {
|
let ctx = BuildContext::new(
|
||||||
build_id,
|
self.tx.clone(),
|
||||||
tx: self.tx.clone(),
|
BuildMode::Thin {
|
||||||
mode: BuildMode::Thin {
|
|
||||||
changed_files,
|
changed_files,
|
||||||
changed_crates,
|
|
||||||
modified_crates: self.modified_crates.clone(),
|
modified_crates: self.modified_crates.clone(),
|
||||||
workspace_rustc_args: artifacts.workspace_rustc_args,
|
workspace_rustc_args: artifacts.workspace_rustc,
|
||||||
aslr_reference,
|
aslr_reference,
|
||||||
cache,
|
cache,
|
||||||
object_cache: self.object_cache.clone(),
|
|
||||||
},
|
},
|
||||||
};
|
build_id,
|
||||||
async move { request.build(&ctx).await }
|
);
|
||||||
|
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.
|
// A full rebuild resets all accumulated hotpatch state — the fat binary is a clean baseline.
|
||||||
self.modified_crates.clear();
|
self.modified_crates.clear();
|
||||||
self.object_cache = ObjectCache::new(&self.build.session_cache_dir());
|
self.profile_spans.clear();
|
||||||
self.build_task = tokio::spawn({
|
self.build_task = tokio::spawn({
|
||||||
let request = self.build.clone();
|
let request = self.build.clone();
|
||||||
let ctx = BuildContext {
|
let tx = self.tx.clone();
|
||||||
tx: self.tx.clone(),
|
async move {
|
||||||
mode,
|
let ctx = BuildContext::new(tx, mode, build_id);
|
||||||
build_id,
|
request.build(ctx).await
|
||||||
};
|
}
|
||||||
async move { request.build(&ctx).await }
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,6 +548,7 @@ impl AppBuilder {
|
|||||||
|
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
BuilderUpdate::ProfilePhase { .. } => {}
|
||||||
BuilderUpdate::StdoutReceived { .. } => {}
|
BuilderUpdate::StdoutReceived { .. } => {}
|
||||||
BuilderUpdate::StderrReceived { .. } => {}
|
BuilderUpdate::StderrReceived { .. } => {}
|
||||||
BuilderUpdate::ProcessExited { .. } => {}
|
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.
|
/// Note that Dioxus apps *should not* rely on this vars being set, but libraries like Bevy do.
|
||||||
pub(crate) fn child_environment_variables(
|
pub(crate) fn child_environment_variables(
|
||||||
&mut self,
|
&self,
|
||||||
devserver_ip: Option<SocketAddr>,
|
devserver_ip: Option<SocketAddr>,
|
||||||
start_fullstack_on_address: Option<SocketAddr>,
|
start_fullstack_on_address: Option<SocketAddr>,
|
||||||
always_on_top: bool,
|
always_on_top: bool,
|
||||||
@@ -605,22 +634,16 @@ impl AppBuilder {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's any CARGO vars in the rustc_wrapper files, push those too.
|
// If there's any CARGO vars in the captured rustc args, push those too.
|
||||||
// Read from any per-crate args file in the directory (they all share the same CARGO_ envs).
|
// Any captured invocation is sufficient since the CARGO_ environment is shared.
|
||||||
if let Ok(entries) = std::fs::read_dir(self.build.rustc_wrapper_args_dir()) {
|
if let Some(args) = self
|
||||||
for entry in entries.flatten() {
|
.artifacts
|
||||||
let path = entry.path();
|
.as_ref()
|
||||||
if path.extension().is_some_and(|e| e == "json") {
|
.and_then(|artifacts| artifacts.workspace_rustc.rustc_args.values().next())
|
||||||
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.iter().cloned() {
|
||||||
for (key, value) in args.envs {
|
if key.starts_with("CARGO_") {
|
||||||
if key.starts_with("CARGO_") {
|
envs.push((key, value));
|
||||||
envs.push((key, value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break; // Only need one file for CARGO_ env vars
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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(
|
pub(crate) async fn hotpatch(
|
||||||
&mut self,
|
&mut self,
|
||||||
res: &BuildArtifacts,
|
res: &BuildArtifacts,
|
||||||
@@ -742,7 +815,7 @@ impl AppBuilder {
|
|||||||
) -> Result<JumpTable> {
|
) -> Result<JumpTable> {
|
||||||
let original = self.build.main_exe();
|
let original = self.build.main_exe();
|
||||||
let new = self.build.patch_exe(res.time_start);
|
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
|
// Hotpatch asset!() calls
|
||||||
for bundled in res.assets.unique_assets() {
|
for bundled in res.assets.unique_assets() {
|
||||||
@@ -822,9 +895,6 @@ impl AppBuilder {
|
|||||||
// Commit this patch
|
// Commit this patch
|
||||||
self.patches.push(jump_table.clone());
|
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)
|
Ok(jump_table)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,7 +920,7 @@ impl AppBuilder {
|
|||||||
// we won't actually be using the build dir.
|
// we won't actually be using the build dir.
|
||||||
let asset_dir = match self.runtime_asset_dir.as_ref() {
|
let asset_dir = match self.runtime_asset_dir.as_ref() {
|
||||||
Some(dir) => dir.to_path_buf().join("assets/"),
|
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:\\".
|
// Canonicalize the path as Windows may use long-form paths "\\\\?\\C:\\".
|
||||||
@@ -1558,32 +1628,23 @@ impl AppBuilder {
|
|||||||
main_exe
|
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
|
/// Get the total duration of the build, if all stages have completed
|
||||||
pub(crate) fn total_build_time(&self) -> Option<Duration> {
|
pub(crate) fn total_build_time(&self) -> Option<Duration> {
|
||||||
Some(self.compile_duration()? + self.bundle_duration()?)
|
Some(self.compile_duration()? + self.bundle_duration()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn compile_duration(&self) -> Option<Duration> {
|
pub(crate) fn compile_duration(&self) -> Option<Duration> {
|
||||||
Some(
|
self.compile_end
|
||||||
self.compile_end
|
.unwrap_or_else(SystemTime::now)
|
||||||
.unwrap_or_else(Instant::now)
|
.duration_since(self.compile_start?)
|
||||||
.duration_since(self.compile_start?),
|
.ok()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn bundle_duration(&self) -> Option<Duration> {
|
pub(crate) fn bundle_duration(&self) -> Option<Duration> {
|
||||||
Some(
|
self.bundle_end
|
||||||
self.bundle_end
|
.unwrap_or_else(SystemTime::now)
|
||||||
.unwrap_or_else(Instant::now)
|
.duration_since(self.bundle_start?)
|
||||||
.duration_since(self.bundle_start?),
|
.ok()
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a number between 0 and 1 representing the progress of the app build
|
/// Return a number between 0 and 1 representing the progress of the app build
|
||||||
@@ -1868,4 +1929,282 @@ impl AppBuilder {
|
|||||||
|
|
||||||
Ok(pid)
|
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 cargo_metadata::diagnostic::Diagnostic;
|
||||||
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||||
use serde::{Deserialize, Serialize};
|
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
|
/// 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
|
/// provides some dynamic configuration that is only known at runtime. For example, the Progress channel
|
||||||
@@ -35,6 +35,10 @@ pub enum BuilderUpdate {
|
|||||||
stage: BuildStage,
|
stage: BuildStage,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ProfilePhase {
|
||||||
|
profile: BuildPhaseProfile,
|
||||||
|
},
|
||||||
|
|
||||||
CompilerMessage {
|
CompilerMessage {
|
||||||
message: Diagnostic,
|
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 {
|
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?
|
/// 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.
|
/// We try not to duplicate work between client and server builds, like asset copying.
|
||||||
pub(crate) fn is_primary_build(&self) -> bool {
|
pub(crate) fn is_primary_build(&self) -> bool {
|
||||||
self.build_id == BuildId::PRIMARY
|
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) {
|
pub(crate) fn status_wasm_bindgen_start(&self) {
|
||||||
|
self.profile_phase("Wasm Bindgen");
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::RunningBindgen,
|
stage: BuildStage::RunningBindgen,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn status_splitting_bundle(&self) {
|
pub(crate) fn status_splitting_bundle(&self) {
|
||||||
|
self.profile_phase("Wasm Split");
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::SplittingBundle,
|
stage: BuildStage::SplittingBundle,
|
||||||
});
|
});
|
||||||
@@ -104,12 +129,14 @@ impl BuildContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn status_running_gradle(&self) {
|
pub(crate) fn status_running_gradle(&self) {
|
||||||
|
self.profile_phase("Gradle");
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::RunningGradle,
|
stage: BuildStage::RunningGradle,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn status_compiling_native_plugins(&self, detail: impl Into<String>) {
|
pub(crate) fn status_compiling_native_plugins(&self, detail: impl Into<String>) {
|
||||||
|
self.profile_phase("Compiling Native Plugins");
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::CompilingNativePlugins {
|
stage: BuildStage::CompilingNativePlugins {
|
||||||
detail: detail.into(),
|
detail: detail.into(),
|
||||||
@@ -118,6 +145,7 @@ impl BuildContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn status_codesigning(&self) {
|
pub(crate) fn status_codesigning(&self) {
|
||||||
|
self.profile_phase("Code Signing");
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::CodeSigning,
|
stage: BuildStage::CodeSigning,
|
||||||
});
|
});
|
||||||
@@ -144,6 +172,7 @@ impl BuildContext {
|
|||||||
name: String,
|
name: String,
|
||||||
fresh: bool,
|
fresh: bool,
|
||||||
) {
|
) {
|
||||||
|
self.profile_phase("Compiling");
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::Compiling {
|
stage: BuildStage::Compiling {
|
||||||
current: count,
|
current: count,
|
||||||
@@ -155,6 +184,7 @@ impl BuildContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn status_starting_build(&self, crate_count: usize) {
|
pub(crate) fn status_starting_build(&self, crate_count: usize) {
|
||||||
|
self.profile_phase("Compiling");
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::Starting {
|
stage: BuildStage::Starting {
|
||||||
patch: matches!(self.mode, BuildMode::Thin { .. }),
|
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 {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::Linking,
|
stage: BuildStage::Linking,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn status_copied_asset(
|
pub(crate) fn status_copied_asset(&self, current: usize, total: usize, path: PathBuf) {
|
||||||
progress: &UnboundedSender<BuilderUpdate>,
|
self.profile_phase("Copying Assets");
|
||||||
current: usize,
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
total: usize,
|
|
||||||
path: PathBuf,
|
|
||||||
) {
|
|
||||||
_ = progress.unbounded_send(BuilderUpdate::Progress {
|
|
||||||
stage: BuildStage::CopyingAssets {
|
stage: BuildStage::CopyingAssets {
|
||||||
current,
|
current,
|
||||||
total,
|
total,
|
||||||
@@ -185,29 +212,34 @@ impl BuildContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn status_optimizing_wasm(&self) {
|
pub(crate) fn status_optimizing_wasm(&self) {
|
||||||
|
self.profile_phase("Optimizing Wasm");
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::OptimizingWasm,
|
stage: BuildStage::OptimizingWasm,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn status_hotpatching(&self) {
|
pub(crate) fn status_writing_patch(&self) {
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::Hotpatching,
|
stage: BuildStage::Hotpatching,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn status_installing_tooling(&self) {
|
pub(crate) fn status_installing_tooling(&self) {
|
||||||
|
self.profile_phase("Installing Tooling");
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::InstallingTooling,
|
stage: BuildStage::InstallingTooling,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn status_compressing_assets(&self) {
|
pub(crate) fn status_compressing_assets(&self) {
|
||||||
|
self.profile_phase("Compressing Assets");
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::CompressingAssets,
|
stage: BuildStage::CompressingAssets,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn status_extracting_assets(&self) {
|
pub(crate) fn status_extracting_assets(&self) {
|
||||||
|
self.profile_phase("Extracting assets");
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::ExtractingAssets,
|
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
|
//! 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.
|
//! 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 assets;
|
||||||
mod builder;
|
mod builder;
|
||||||
mod cache;
|
|
||||||
mod context;
|
mod context;
|
||||||
mod ios_swift;
|
mod link;
|
||||||
mod manifest;
|
|
||||||
mod manifest_mapper;
|
|
||||||
mod patch;
|
mod patch;
|
||||||
mod pre_render;
|
mod renderer;
|
||||||
mod request;
|
mod request;
|
||||||
mod tools;
|
|
||||||
|
|
||||||
|
pub(crate) use android::AndroidTools;
|
||||||
pub(crate) use assets::*;
|
pub(crate) use assets::*;
|
||||||
pub(crate) use builder::*;
|
pub(crate) use builder::*;
|
||||||
pub(crate) use cache::*;
|
|
||||||
pub(crate) use context::*;
|
pub(crate) use context::*;
|
||||||
pub(crate) use manifest::*;
|
|
||||||
pub(crate) use patch::*;
|
pub(crate) use patch::*;
|
||||||
pub(crate) use pre_render::*;
|
|
||||||
pub(crate) use request::*;
|
pub(crate) use request::*;
|
||||||
pub(crate) use tools::*;
|
|
||||||
@@ -9,7 +9,8 @@ use object::{
|
|||||||
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashMap, HashSet},
|
collections::{BTreeMap, HashMap, HashSet},
|
||||||
ops::{Deref, Range},
|
io::Read,
|
||||||
|
ops::Range,
|
||||||
path::Path,
|
path::Path,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, RwLock},
|
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!
|
// Clear the start function from the patch - we don't want any code automatically running!
|
||||||
new.start = None;
|
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
|
// Update the wasm module on the filesystem to use the newly lifted version
|
||||||
let lib = patch.to_path_buf();
|
let lib = patch.to_path_buf();
|
||||||
std::fs::write(&lib, new.emit_wasm())?;
|
std::fs::write(&lib, new.emit_wasm())?;
|
||||||
@@ -846,15 +862,7 @@ pub fn create_undefined_symbol_stub(
|
|||||||
let mut defined_symbols = HashSet::new();
|
let mut defined_symbols = HashSet::new();
|
||||||
|
|
||||||
for path in sorted {
|
for path in sorted {
|
||||||
let bytes = std::fs::read(path).with_context(|| format!("failed to read {path:?}"))?;
|
collect_stub_symbols_from_path(path, &mut undefined_symbols, &mut defined_symbols)?;
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let undefined_symbols: Vec<_> = undefined_symbols
|
let undefined_symbols: Vec<_> = undefined_symbols
|
||||||
.difference(&defined_symbols)
|
.difference(&defined_symbols)
|
||||||
@@ -1251,6 +1259,54 @@ pub fn create_undefined_symbol_stub(
|
|||||||
Ok(obj.write()?)
|
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.
|
/// Prepares the base module before running wasm-bindgen.
|
||||||
///
|
///
|
||||||
/// This tries to work around how wasm-bindgen works by intelligently promoting non-wasm-bindgen functions
|
/// 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
|
// Build the resources map from assets + config resources
|
||||||
let mut resources_map = HashMap::new();
|
let mut resources_map = HashMap::new();
|
||||||
|
|
||||||
let asset_dir = build.asset_dir();
|
let asset_dir = build.bundle_asset_dir();
|
||||||
if asset_dir.exists() {
|
if asset_dir.exists() {
|
||||||
for entry in walkdir::WalkDir::new(&asset_dir) {
|
for entry in walkdir::WalkDir::new(&asset_dir) {
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ impl CommandWithPlatformOverrides<BuildArgs> {
|
|||||||
|
|
||||||
// Run SSG and cache static routes
|
// Run SSG and cache static routes
|
||||||
if ssg {
|
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! 🚀");
|
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,
|
// 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
|
// 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.
|
// (only the tip crate has link_args attached) or fall back to any entry.
|
||||||
let tip_args = self
|
let (rustc_args, rustc_envs) = self
|
||||||
.workspace_rustc_args
|
.workspace_rustc
|
||||||
.values()
|
.rustc_args
|
||||||
.find(|a| !a.link_args.is_empty())
|
.iter()
|
||||||
.or_else(|| self.workspace_rustc_args.values().next())
|
.find(|(k, _v)| k.ends_with(".bin"))
|
||||||
.cloned()
|
.map(|f| (f.1.args.clone(), f.1.envs.clone()))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
StructuredBuildArtifacts {
|
StructuredBuildArtifacts {
|
||||||
path: self.root_dir,
|
path: self.root_dir,
|
||||||
exe: self.exe,
|
exe: self.exe,
|
||||||
rustc_args: tip_args.args,
|
rustc_args,
|
||||||
rustc_envs: tip_args.envs,
|
rustc_envs,
|
||||||
link_args: tip_args.link_args,
|
link_args: self.workspace_rustc.link_args,
|
||||||
assets: self.assets.unique_assets().cloned().collect(),
|
assets: self.assets.unique_assets().cloned().collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::{fs::create_dir_all, path::PathBuf};
|
use std::{fs::create_dir_all, path::PathBuf};
|
||||||
|
|
||||||
use crate::opt::process_file_to;
|
use crate::opt::process_file_to;
|
||||||
use crate::{extract_assets_from_file, Result, StructuredOutput};
|
use crate::{Result, StructuredOutput};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
@@ -16,9 +16,10 @@ pub struct BuildAssets {
|
|||||||
|
|
||||||
impl BuildAssets {
|
impl BuildAssets {
|
||||||
pub async fn run(self) -> Result<StructuredOutput> {
|
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)?;
|
create_dir_all(&self.destination)?;
|
||||||
|
|
||||||
for asset in manifest.unique_assets() {
|
for asset in manifest.unique_assets() {
|
||||||
let source_path = PathBuf::from(asset.absolute_source_path());
|
let source_path = PathBuf::from(asset.absolute_source_path());
|
||||||
let destination_path = self.destination.join(asset.bundled_path());
|
let destination_path = self.destination.join(asset.bundled_path());
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{Result, Workspace};
|
use crate::{AndroidTools, Result, Workspace};
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
@@ -91,24 +91,24 @@ impl Doctor {
|
|||||||
let mut sdk = "not found".to_string();
|
let mut sdk = "not found".to_string();
|
||||||
let mut java_home = "not found".to_string();
|
let mut java_home = "not found".to_string();
|
||||||
let mut emulator = "not found".to_string();
|
let mut emulator = "not found".to_string();
|
||||||
if let Some(rf) = crate::build::get_android_tools() {
|
if let Some(tools) = AndroidTools::current() {
|
||||||
if rf.adb.exists() {
|
if tools.adb.exists() {
|
||||||
adb = rf.adb.display().to_string();
|
adb = tools.adb.display().to_string();
|
||||||
}
|
}
|
||||||
if rf.ndk.exists() {
|
if tools.ndk.exists() {
|
||||||
ndk = rf.ndk.display().to_string();
|
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();
|
java_home = jh.display().to_string();
|
||||||
}
|
}
|
||||||
if rf.sdk().exists() {
|
if tools.sdk().exists() {
|
||||||
sdk = rf.sdk().display().to_string();
|
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();
|
java_home = jh.display().to_string();
|
||||||
}
|
}
|
||||||
if rf.emulator().exists() {
|
if tools.emulator().exists() {
|
||||||
emulator = rf.emulator().display().to_string();
|
emulator = tools.emulator().display().to_string();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
platform_override::CommandWithPlatformOverrides, AppBuilder, BuildArgs, BuildId, BuildMode,
|
platform_override::CommandWithPlatformOverrides, AppBuilder, BuildArgs, BuildId, BuildMode,
|
||||||
HotpatchModuleCache, Result, StructuredOutput,
|
HotpatchModuleCache, Result, StructuredOutput, WorkspaceRustcArgs,
|
||||||
};
|
};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use dioxus_dx_wire_format::StructuredBuildArtifacts;
|
use dioxus_dx_wire_format::StructuredBuildArtifacts;
|
||||||
|
use std::io::Read;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{collections::HashMap, io::Read};
|
|
||||||
|
|
||||||
const HELP_HEADING: &str = "Hotpatching a binary";
|
const HELP_HEADING: &str = "Hotpatching a binary";
|
||||||
|
|
||||||
@@ -71,24 +71,21 @@ impl HotpatchTip {
|
|||||||
let cache = Arc::new(HotpatchModuleCache::new(&exe, &request.triple)?);
|
let cache = Arc::new(HotpatchModuleCache::new(&exe, &request.triple)?);
|
||||||
|
|
||||||
let tip_crate_name = request.main_target.replace('-', "_");
|
let tip_crate_name = request.main_target.replace('-', "_");
|
||||||
let mut workspace_rustc_args = HashMap::new();
|
let mut workspace_rustc_args = WorkspaceRustcArgs::new(link_args);
|
||||||
workspace_rustc_args.insert(
|
workspace_rustc_args.rustc_args.insert(
|
||||||
format!("{tip_crate_name}.bin"),
|
format!("{tip_crate_name}.bin"),
|
||||||
crate::RustcArgs {
|
crate::RustcArgs {
|
||||||
args: rustc_args,
|
args: rustc_args,
|
||||||
envs: rustc_envs,
|
envs: rustc_envs,
|
||||||
link_args,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let mode = BuildMode::Thin {
|
let mode = BuildMode::Thin {
|
||||||
workspace_rustc_args,
|
workspace_rustc_args,
|
||||||
changed_files: vec![],
|
changed_files: vec![],
|
||||||
changed_crates: vec![],
|
|
||||||
modified_crates: std::collections::HashSet::new(),
|
modified_crates: std::collections::HashSet::new(),
|
||||||
aslr_reference: self.aslr_reference,
|
aslr_reference: self.aslr_reference,
|
||||||
cache: cache.clone(),
|
cache: cache.clone(),
|
||||||
object_cache: crate::ObjectCache::new(&request.session_cache_dir()),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let artifacts = AppBuilder::started(request, mode, build_id)?
|
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(
|
pub(crate) fn write_env_vars(
|
||||||
&self,
|
&self,
|
||||||
env_vars: &mut Vec<(Cow<'static, str>, OsString)>,
|
env_vars: &mut Vec<(Cow<'static, str>, OsString)>,
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ impl RunArgs {
|
|||||||
BuilderUpdate::ProcessWaitFailed { err } => {
|
BuilderUpdate::ProcessWaitFailed { err } => {
|
||||||
return Err(err.into());
|
return Err(err.into());
|
||||||
}
|
}
|
||||||
|
BuilderUpdate::ProfilePhase { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ServeUpdate::Exit { .. } => break,
|
ServeUpdate::Exit { .. } => break,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use crate::config::{
|
|||||||
pub struct AndroidPermissionEntry {
|
pub struct AndroidPermissionEntry {
|
||||||
/// Full Android permission string (e.g., "android.permission.CAMERA")
|
/// Full Android permission string (e.g., "android.permission.CAMERA")
|
||||||
pub permission: String,
|
pub permission: String,
|
||||||
|
|
||||||
/// User-facing description (used for documentation)
|
/// User-facing description (used for documentation)
|
||||||
pub description: String,
|
pub description: String,
|
||||||
}
|
}
|
||||||
@@ -24,6 +25,7 @@ pub struct AndroidPermissionEntry {
|
|||||||
pub struct PlistEntry {
|
pub struct PlistEntry {
|
||||||
/// Plist key (e.g., "NSCameraUsageDescription")
|
/// Plist key (e.g., "NSCameraUsageDescription")
|
||||||
pub key: String,
|
pub key: String,
|
||||||
|
|
||||||
/// User-facing description shown in permission dialogs
|
/// User-facing description shown in permission dialogs
|
||||||
pub value: String,
|
pub value: String,
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ mod component;
|
|||||||
mod dioxus_config;
|
mod dioxus_config;
|
||||||
mod inline_config;
|
mod inline_config;
|
||||||
mod manifest;
|
mod manifest;
|
||||||
|
mod manifest_mapper;
|
||||||
mod serve;
|
mod serve;
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
@@ -12,5 +13,6 @@ pub(crate) use bundle::*;
|
|||||||
pub(crate) use dioxus_config::*;
|
pub(crate) use dioxus_config::*;
|
||||||
pub(crate) use inline_config::*;
|
pub(crate) use inline_config::*;
|
||||||
pub(crate) use manifest::*;
|
pub(crate) use manifest::*;
|
||||||
|
pub(crate) use manifest_mapper::*;
|
||||||
pub(crate) use serve::*;
|
pub(crate) use serve::*;
|
||||||
pub(crate) use web::*;
|
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 serde::{Deserialize, Serialize};
|
||||||
use std::collections::{BTreeMap, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -14,16 +15,33 @@ mod json;
|
|||||||
pub(crate) use file::process_file_to;
|
pub(crate) use file::process_file_to;
|
||||||
pub(crate) use hash::add_hash_to_asset;
|
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.
|
/// This will be filled in primarily by incremental compilation artifacts.
|
||||||
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
pub(crate) struct AssetManifest {
|
pub(crate) struct AppManifest {
|
||||||
|
/// Stable since 0.7.0
|
||||||
|
pub cli_version: String,
|
||||||
|
|
||||||
/// Map of bundled asset name to the asset itself
|
/// 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
|
/// Manually add an asset to the manifest
|
||||||
pub fn register_asset(
|
pub fn register_asset(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|||||||
@@ -1,10 +1,34 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
env::{args, vars},
|
env::{args, vars},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process::ExitCode,
|
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.
|
/// The environment variable indicating where the args directory is located.
|
||||||
///
|
///
|
||||||
/// When `dx-rustc` runs, it writes each workspace crate's arguments to a
|
/// 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()
|
std::env::var(DX_RUSTC_WRAPPER_ENV_VAR).is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
/// Run rustc directly, but output the result to a per-crate file in the args directory.
|
||||||
pub struct RustcArgs {
|
///
|
||||||
pub args: Vec<String>,
|
/// <https://doc.rust-lang.org/cargo/reference/config.html#buildrustc>
|
||||||
pub envs: Vec<(String, String)>,
|
pub fn run_rustc() -> ExitCode {
|
||||||
/// it doesn't include first program name argument
|
let args_dir: PathBuf = std::env::var(DX_RUSTC_WRAPPER_ENV_VAR)
|
||||||
pub link_args: Vec<String>,
|
.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.
|
/// Check if the arguments indicate a linking step, including those in command files.
|
||||||
@@ -64,85 +168,3 @@ fn has_linking_args() -> bool {
|
|||||||
|
|
||||||
false
|
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::Progress { .. } => {}
|
||||||
|
BuilderUpdate::ProfilePhase { .. } => {}
|
||||||
BuilderUpdate::CompilerMessage { message } => {
|
BuilderUpdate::CompilerMessage { message } => {
|
||||||
screen.push_cargo_log(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);
|
screen.push_log(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
ServeUpdate::OpenApp => match builder.use_hotpatch_engine {
|
ServeUpdate::OpenApp => {
|
||||||
true if !matches!(builder.client.build.bundle, BundleFormat::Web) => {
|
if builder.use_hotpatch_engine
|
||||||
|
&& !matches!(builder.client.build.bundle, BundleFormat::Web)
|
||||||
|
{
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"Opening a native app with hotpatching enabled requires a full rebuild..."
|
"Opening a native app with hotpatching enabled requires a full rebuild..."
|
||||||
);
|
);
|
||||||
builder.full_rebuild().await;
|
builder.full_rebuild().await;
|
||||||
devserver.send_reload_start().await;
|
devserver.send_reload_start().await;
|
||||||
devserver.start_build().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 => {
|
ServeUpdate::Redraw => {
|
||||||
// simply returning will cause a redraw
|
// simply returning will cause a redraw
|
||||||
|
|||||||
@@ -259,12 +259,12 @@ impl AppServer {
|
|||||||
if !self.ssg || server.stage != BuildStage::Success {
|
if !self.ssg || server.stage != BuildStage::Success {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Err(err) = crate::pre_render_static_routes(
|
if let Err(err) = server
|
||||||
Some(devserver.devserver_address()),
|
.pre_render_static_routes(
|
||||||
server,
|
Some(devserver.devserver_address()),
|
||||||
Some(&server.tx.clone()),
|
Some(&server.tx.clone()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
tracing::error!("Failed to pre-render static routes: {err}");
|
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
|
// This prevents losing changes from tools like stylance, tailwind, or sass that generate files
|
||||||
// in response to source changes.
|
// in response to source changes.
|
||||||
tracing::debug!(
|
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
|
files
|
||||||
);
|
);
|
||||||
self.pending_file_changes.extend(files.iter().cloned());
|
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
|
// Get the cached file if it exists - ignoring if it doesn't exist
|
||||||
let Some(cached_file) = self.file_map.get_mut(path) else {
|
let Some(cached_file) = self.file_map.get_mut(path) else {
|
||||||
tracing::debug!("No entry for file in filemap: {:?}", path);
|
tracing::debug!("No entry for file in filemap: {:?}", path);
|
||||||
tracing::debug!("Filemap: {:#?}", self.file_map.keys());
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -601,10 +600,12 @@ impl AppServer {
|
|||||||
use crate::cli::styles::GLOW_STYLE;
|
use crate::cli::styles::GLOW_STYLE;
|
||||||
|
|
||||||
if should_open {
|
if should_open {
|
||||||
let time_taken = artifacts
|
let time_taken = self.client.total_build_time().unwrap_or_else(|| {
|
||||||
.time_end
|
artifacts
|
||||||
.duration_since(artifacts.time_start)
|
.time_end
|
||||||
.unwrap();
|
.duration_since(artifacts.time_start)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
if self.client.builds_opened == 0 {
|
if self.client.builds_opened == 0 {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ impl WebServer {
|
|||||||
BuilderUpdate::StderrReceived { .. } => {}
|
BuilderUpdate::StderrReceived { .. } => {}
|
||||||
BuilderUpdate::ProcessExited { .. } => {}
|
BuilderUpdate::ProcessExited { .. } => {}
|
||||||
BuilderUpdate::ProcessWaitFailed { .. } => {}
|
BuilderUpdate::ProcessWaitFailed { .. } => {}
|
||||||
|
BuilderUpdate::ProfilePhase { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ async fn test_harnesses() {
|
|||||||
assert_eq!(targets.client.triple, "aarch64-apple-ios".parse().unwrap());
|
assert_eq!(targets.client.triple, "aarch64-apple-ios".parse().unwrap());
|
||||||
})
|
})
|
||||||
.asrt(r#"dx build --android --device"#, |targets| async move {
|
.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
|
// Skip when Android NDK/SDK is not installed
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -254,7 +254,6 @@ async fn test_harnesses() {
|
|||||||
assert!(t.server.is_none());
|
assert!(t.server.is_none());
|
||||||
assert_eq!(t.client.bundle, BundleFormat::Ios);
|
assert_eq!(t.client.bundle, BundleFormat::Ios);
|
||||||
assert_eq!(t.client.triple, TestHarnessBuilder::host_ios_triple_sim());
|
assert_eq!(t.client.triple, TestHarnessBuilder::host_ios_triple_sim());
|
||||||
assert!(t.client.no_default_features);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TestHarnessBuilder::new("harness-fullstack-with-optional-tokio")
|
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 {
|
Self {
|
||||||
keep_lld_exports: keep_lld_sections,
|
keep_lld_exports,
|
||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use tar::Archive;
|
|||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
/// Pinned binaryen version (contains wasm-opt).
|
/// 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
|
/// Write these wasm bytes with a particular set of optimizations
|
||||||
pub async fn write_wasm(bytes: &[u8], output_path: &Path, cfg: &WasmOptConfig) -> Result<()> {
|
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")?;
|
.context("Failed to load Cargo.toml")?;
|
||||||
|
|
||||||
let android_tools = crate::build::get_android_tools();
|
let android_tools = AndroidTools::current();
|
||||||
|
|
||||||
let workspace = Arc::new(Self {
|
let workspace = Arc::new(Self {
|
||||||
krates,
|
krates,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::BundledAsset;
|
use crate::BundledAsset;
|
||||||
use const_serialize::{ConstStr, SerializeConst};
|
use const_serialize::{ConstStr, SerializeConst};
|
||||||
use const_serialize_08 as const_serialize;
|
use const_serialize_08 as const_serialize;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
|
||||||
/// Unified symbol data that can represent both assets and permissions
|
/// Unified symbol data that can represent both assets and permissions
|
||||||
@@ -79,7 +80,7 @@ pub struct PlatformIdentifiers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Metadata describing an Android plugin artifact (.aar) that must be copied into the host Gradle project.
|
/// Metadata describing an Android plugin artifact (.aar) that must be copied into the host Gradle project.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst, Serialize, Deserialize)]
|
||||||
pub struct AndroidArtifactMetadata {
|
pub struct AndroidArtifactMetadata {
|
||||||
pub plugin_name: ConstStr,
|
pub plugin_name: ConstStr,
|
||||||
pub artifact_path: ConstStr,
|
pub artifact_path: ConstStr,
|
||||||
@@ -101,7 +102,7 @@ impl AndroidArtifactMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Metadata for a Swift package that needs to be linked into the app (iOS/macOS).
|
/// Metadata for a Swift package that needs to be linked into the app (iOS/macOS).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst, Serialize, Deserialize)]
|
||||||
pub struct SwiftPackageMetadata {
|
pub struct SwiftPackageMetadata {
|
||||||
pub plugin_name: ConstStr,
|
pub plugin_name: ConstStr,
|
||||||
pub package_path: ConstStr,
|
pub package_path: ConstStr,
|
||||||
|
|||||||
Reference in new issue
Block a user