Binary patching rust hot-reloading, sub-second rebuilds, independent server/client hot-reload (#3797)
Enable runtime rust hot patching. * roll back wry to 0.45 * cleanup rustc * wip * wip: fix client/server * get the subsecond cli thing working again * back to list of builds but with accessors * migrate some methods to workspace, clean up impl a bit * pass mode through build context * use a build context, similar to cargo/rustc * merge BuildUpdate and HandleUpdate into BuilderUpdate * Move more resolution of args into runner * migrate some methods to runner * hoist out fullstack * fix build request resolver * yay request is cleaned up * fixup resolution a bit * spawn off the build! * re-wire android autodetection * re-wire android autodetection * wire back up a few more things * re-wire tooling verification * I think it's mostly in the right condition for regular app * add depinfo parser * yay okay works with regular apps again * full rebuilds are back * rewire file wtcher * wire patch! * yayyyyyy patching works and is really fast * yay android works * clean up stdout piping * yayyy memap on android so we don't need root * create a static wasm jump table * wip: global offset table for wasm * wip... thinking about making the patches relocatable * wip: customized relocation * YESSSSSS RELOCATABLE WASM * clean up impl a bit * lil bit more cleanup * lil bit more * lil bit more cleanup * lil bit more cleanup, support 32 bit platforms * sick, wasm is completely pic * hmmmm not quite working yet * woooo, patches loading from rust+wasm * integrated into the cli 😎 * condense a bit * cleaned up request a bit * bust fingerprints * Make the file diffing logic a bit more sensible * still working through a few issues, server launches but is acting weird * fix merge conflict * more merge issues * remove stuff we don't want anymore * revert name change * wip: server not working anymore :( * split apart "dioxus fullstack" into "dioxus server" and "dioxus web" * fixup a few more compile errors * grrr opening server * simultaneous frontend and backend patching * use rustc wrapper * wip: passing assets to jumptable * migrate project-like examples * patchy patchy server fns! * rollback some random changes * unwind the js files * rollback hash * no need for patch * more cleanups * lil bit more cleanup, remove some old cruft * allow patching when wasm webpage is closed * tiny bit more robust * lil bit of clean up * clean up ws code * bit more clean ups * condense * undo file moves * move back other project * move other project * migrate out harness and janky CLI * fix compile * anonymize some files, heavily document others * lots more documentation, for posterity! * ton more docs * clean up the cli a bit more * more ws cleanup * more cleanup * migrate build id to cli-config * add command file handling * random small cleanups * wip.... * wip.... * fix: use workspace for krate discovery * fix panic logging on serve by placing it *after* the logs * reorder logging to be sensible on success * bring back swc * ws cruft * small patches to relocs in loader * bump krates * hoist main funcs into the ifunc table, fixing wasm * fix tui output for long lines * add more linker logging * wow, incredible, we build fat binaries, holy heckkkkk * fix args for frontend/backend * small cleanups * properly send along build IDs to clients * fix workspace compiles * clean up jump table work a bit * clean up logging a bit * open existing * fixup some commands * open existing browser integration * wire up the new dx serve client/server syntax * fixup `dx run` command * bring back some old functionality * fix serverfn feature sets * remove toast animation and simplify its code a bit * less intrusive toast * dont change build status when patching on web * add proper cache busting to vcomponent * clean up the patch file a bit * more lints/checks cleaned up * go back to TargetArgs but a BuildTargets resolved struct * clean up more nits * use an atomicptr for jumptable * fix interaction with initial web load + suspense * don't run asset system on wasm/bindgen js in dev * reduce blast radius of PR * use profile to determine if we're in release * cleanup prod html template * cleanup profiles * fix feature resolution of fullstack * if fullstack is explicitly false, don't use it * light cleanups * drop carg config2 * pass along linker args properly * make workspace examples compile with dx again * fewer unwraps and better error loggign * bit more error handlign * small cleanup * drive-by cleanups * use queries instead of initialize for aslr stuff * fix hotpatch with higher opt level * fix aslr bug from multiple clients * fix merge conflict * fix typos * clippy * fix miscompile * fix doctest * properly rollback wry to 0.45 * fix markdown path issues, other test issues * fix test in router * fix release mode test * fix some more tests, clean up a few more items * use fnptr instead of typeid * clean up memozation via subsecond in core * use "main" as sentinel * fix imports and re-exports * get off __rust_alloc for aslr offset detection * wip * wip... fixing unoptimized web * hmmmm mmmm * close,ish, still missing the wasmbindgen dynaimc imports * aha! full wasm works now - disable externref when compilng patch modules - manually link wasm instrinsics - displace the __wbindgen_placeholder__ table with the synthesized wbg imports what an awful pain. There might be a few items missing, so will test against the docsite shortly * remove failing test * hmmmm things aren't perfect * from what I can tell, relocatable wasm just doesn't work with wasm-bindgen sad https://github.com/rustwasm/wasm-bindgen/issues/1420 * AI IS LITERALLY THE WORST * preserve wasm snippets across compiles * IT HOTPATCHES THE DOCSITE, WE ARE LIVING IN THE FUTURE * remove the macos cfg-out * fix: need to allocate 1 more page * fix url issue * properly span props * properly fill in extra symbols * light cleanups and good docs * delete cruft, better docs * More cleanups and simplifications * clippy, typos * remove harnesses * wasm-bindgen error logging * clean up deps * remove cruft * proper priv/pub * use ifuncs for all env imports * cruft * use par_iter, better output * implement a cache for the base module * delete custom sections * cache the main native module too * add par iter in a few more places * clippy, map ident * better logging and status bars * wip: some small windows support * wip more windowss * fix weird regression * implement windows jump table * small fixes for windows * better windows support * windows support pt2 * rust lld is not msvc * whole archive the rlibs * ignore windows and sys crates * logging * ohhhhhh, pop out dlls * dedupe * thANK GOD FAT LINKING WORKS ON WINDOWS * hoist caching * implement patch based on cache * fix location for pdb * pass data symbols, correct object * add all symbol for windows * add log * treat none rva as undefined * whoops * handle windows calling convention * dont use relocated arm64 stub windows * not all are text? * WINDOWS FUNCTION PATCHING WORKS but issue with statics? * use our own entry * handle windows imp symbols * add log * jump to the real symbol not the layout * whoops * hotpatching windows works completely holy heck yay * drop linker args crate * disable ssg playwright for now * light cleanups * some cleanups / preps: - bump to serverfn 0.8.2 - fix playwright test - fix clippy * use fix from #4042 * bring in other fix for liveview * hoist some configuration * use __aslr_reference instead of aslr_reference * add better errors to patcherror * fix android * panic if patching fails * clean up tomls * wip: fixing wasm compile for server * fullstack wasm again * fix cfg * fixup feature detection * fix issue with features * fix playwright! * add barebones template test harness * fix compatibility between the two hotreload engines * fix preldue of server * fix check for wasm-opt * small fixes
This commit is contained in:
1 parent
71d85e6dd5
commit
d976ca184a
142 files changed
+17982
-7156
No files matched your search
+1078
-90
File diff suppressed because it is too large.
Load diff
@@ -1,955 +0,0 @@
|
||||
use super::prerender::pre_render_static_routes;
|
||||
use super::templates::InfoPlistData;
|
||||
use crate::{BuildRequest, Platform, WasmOptConfig};
|
||||
use crate::{Result, TraceSrc};
|
||||
use anyhow::Context;
|
||||
use dioxus_cli_opt::{process_file_to, AssetManifest};
|
||||
use manganis::{AssetOptions, JsAssetOptions};
|
||||
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::{collections::HashSet, io::Write};
|
||||
use std::{sync::atomic::AtomicUsize, time::Duration};
|
||||
use tokio::process::Command;
|
||||
|
||||
/// The end result of a build.
|
||||
///
|
||||
/// Contains the final asset manifest, the executables, and the workdir.
|
||||
///
|
||||
/// Every dioxus app can have an optional server executable which will influence the final bundle.
|
||||
/// This is built in parallel with the app executable during the `build` phase and the progres/status
|
||||
/// of the build is aggregated.
|
||||
///
|
||||
/// The server will *always* be dropped into the `web` folder since it is considered "web" in nature,
|
||||
/// and will likely need to be combined with the public dir to be useful.
|
||||
///
|
||||
/// We do our best to assemble read-to-go bundles here, such that the "bundle" step for each platform
|
||||
/// can just use the build dir
|
||||
///
|
||||
/// When we write the AppBundle to a folder, it'll contain each bundle for each platform under the app's name:
|
||||
/// ```
|
||||
/// dog-app/
|
||||
/// build/
|
||||
/// web/
|
||||
/// server.exe
|
||||
/// assets/
|
||||
/// some-secret-asset.txt (a server-side asset)
|
||||
/// public/
|
||||
/// index.html
|
||||
/// assets/
|
||||
/// logo.png
|
||||
/// desktop/
|
||||
/// App.app
|
||||
/// App.appimage
|
||||
/// App.exe
|
||||
/// server/
|
||||
/// server
|
||||
/// assets/
|
||||
/// some-secret-asset.txt (a server-side asset)
|
||||
/// ios/
|
||||
/// App.app
|
||||
/// App.ipa
|
||||
/// android/
|
||||
/// App.apk
|
||||
/// bundle/
|
||||
/// build.json
|
||||
/// Desktop.app
|
||||
/// Mobile_x64.ipa
|
||||
/// Mobile_arm64.ipa
|
||||
/// Mobile_rosetta.ipa
|
||||
/// web.appimage
|
||||
/// web/
|
||||
/// server.exe
|
||||
/// assets/
|
||||
/// some-secret-asset.txt
|
||||
/// public/
|
||||
/// index.html
|
||||
/// assets/
|
||||
/// logo.png
|
||||
/// style.css
|
||||
/// ```
|
||||
///
|
||||
/// When deploying, the build.json file will provide all the metadata that dx-deploy will use to
|
||||
/// push the app to stores, set up infra, manage versions, etc.
|
||||
///
|
||||
/// The format of each build will follow the name plus some metadata such that when distributing you
|
||||
/// can easily trim off the metadata.
|
||||
///
|
||||
/// The idea here is that we can run any of the programs in the same way that they're deployed.
|
||||
///
|
||||
///
|
||||
/// ## Bundle structure links
|
||||
/// - apple: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle
|
||||
/// - appimage: https://docs.appimage.org/packaging-guide/manual.html#ref-manual
|
||||
///
|
||||
/// ## Extra links
|
||||
/// - xbuild: https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AppBundle {
|
||||
pub(crate) build: BuildRequest,
|
||||
pub(crate) app: BuildArtifacts,
|
||||
pub(crate) server: Option<BuildArtifacts>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BuildArtifacts {
|
||||
pub(crate) exe: PathBuf,
|
||||
pub(crate) assets: AssetManifest,
|
||||
pub(crate) time_taken: Duration,
|
||||
}
|
||||
|
||||
impl AppBundle {
|
||||
/// ## 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
|
||||
/// ```
|
||||
///
|
||||
/// ## Macos
|
||||
/// We simply use the macos format where binaries are in `Contents/MacOS` and assets are in `Contents/Resources`
|
||||
/// We put assets in an assets dir such that it generally matches every other platform and we can
|
||||
/// output `/assets/blah` from manganis.
|
||||
/// ```
|
||||
/// App.app/
|
||||
/// Contents/
|
||||
/// Info.plist
|
||||
/// MacOS/
|
||||
/// Frameworks/
|
||||
/// Resources/
|
||||
/// assets/
|
||||
/// blah.icns
|
||||
/// blah.png
|
||||
/// CodeResources
|
||||
/// _CodeSignature/
|
||||
/// ```
|
||||
///
|
||||
/// ## iOS
|
||||
/// Not the same as mac! ios apps are a bit "flattened" in comparison. simpler format, presumably
|
||||
/// since most ios apps don't ship frameworks/plugins and such.
|
||||
///
|
||||
/// todo(jon): include the signing and entitlements in this format diagram.
|
||||
/// ```
|
||||
/// App.app/
|
||||
/// main
|
||||
/// assets/
|
||||
/// ```
|
||||
///
|
||||
/// ## Android:
|
||||
///
|
||||
/// Currently we need to generate a `src` type structure, not a pre-packaged apk structure, since
|
||||
/// we need to compile kotlin and java. This pushes us into using gradle and following a structure
|
||||
/// similar to that of cargo mobile2. Eventually I'd like to slim this down (drop buildSrc) and
|
||||
/// drive the kotlin build ourselves. This would let us drop gradle (yay! no plugins!) but requires
|
||||
/// us to manage dependencies (like kotlinc) ourselves (yuck!).
|
||||
///
|
||||
/// https://github.com/WanghongLin/miscellaneous/blob/master/tools/build-apk-manually.sh
|
||||
///
|
||||
/// Unfortunately, it seems that while we can drop the `android` build plugin, we still will need
|
||||
/// gradle since kotlin is basically gradle-only.
|
||||
///
|
||||
/// Pre-build:
|
||||
/// ```
|
||||
/// app.apk/
|
||||
/// .gradle
|
||||
/// app/
|
||||
/// src/
|
||||
/// main/
|
||||
/// assets/
|
||||
/// jniLibs/
|
||||
/// java/
|
||||
/// kotlin/
|
||||
/// res/
|
||||
/// AndroidManifest.xml
|
||||
/// build.gradle.kts
|
||||
/// proguard-rules.pro
|
||||
/// buildSrc/
|
||||
/// build.gradle.kts
|
||||
/// src/
|
||||
/// main/
|
||||
/// kotlin/
|
||||
/// BuildTask.kt
|
||||
/// build.gradle.kts
|
||||
/// gradle.properties
|
||||
/// gradlew
|
||||
/// gradlew.bat
|
||||
/// settings.gradle
|
||||
/// ```
|
||||
///
|
||||
/// Final build:
|
||||
/// ```
|
||||
/// app.apk/
|
||||
/// AndroidManifest.xml
|
||||
/// classes.dex
|
||||
/// assets/
|
||||
/// logo.png
|
||||
/// lib/
|
||||
/// armeabi-v7a/
|
||||
/// libmyapp.so
|
||||
/// arm64-v8a/
|
||||
/// libmyapp.so
|
||||
/// ```
|
||||
/// Notice that we *could* feasibly build this ourselves :)
|
||||
///
|
||||
/// ## Windows:
|
||||
/// https://superuser.com/questions/749447/creating-a-single-file-executable-from-a-directory-in-windows
|
||||
/// Windows does not provide an AppImage format, so instead we're going build the same folder
|
||||
/// structure as an AppImage, but when distributing, we'll create a .exe that embeds the resources
|
||||
/// as an embedded .zip file. When the app runs, it will implicitly unzip its resources into the
|
||||
/// Program Files folder. Any subsequent launches of the parent .exe will simply call the AppRun.exe
|
||||
/// entrypoint in the associated Program Files folder.
|
||||
///
|
||||
/// This is, in essence, the same as an installer, so we might eventually just support something like msi/msix
|
||||
/// which functionally do the same thing but with a sleeker UI.
|
||||
///
|
||||
/// This means no installers are required and we can bake an updater into the host exe.
|
||||
///
|
||||
/// ## Handling asset lookups:
|
||||
/// current_exe.join("assets")
|
||||
/// ```
|
||||
/// app.appimage/
|
||||
/// main.exe
|
||||
/// main.desktop
|
||||
/// package.json
|
||||
/// assets/
|
||||
/// logo.png
|
||||
/// ```
|
||||
///
|
||||
/// Since we support just a few locations, we could just search for the first that exists
|
||||
/// - usr
|
||||
/// - ../Resources
|
||||
/// - assets
|
||||
/// - Assets
|
||||
/// - $cwd/assets
|
||||
///
|
||||
/// ```
|
||||
/// assets::root() ->
|
||||
/// mac -> ../Resources/
|
||||
/// ios -> ../Resources/
|
||||
/// android -> assets/
|
||||
/// server -> assets/
|
||||
/// liveview -> assets/
|
||||
/// web -> /assets/
|
||||
/// root().join(bundled)
|
||||
/// ```
|
||||
pub(crate) async fn new(
|
||||
build: BuildRequest,
|
||||
app: BuildArtifacts,
|
||||
server: Option<BuildArtifacts>,
|
||||
) -> Result<Self> {
|
||||
let mut bundle = Self { app, server, build };
|
||||
|
||||
tracing::debug!("Assembling app bundle");
|
||||
|
||||
bundle.build.status_start_bundle();
|
||||
/*
|
||||
assume the build dir is already created by BuildRequest
|
||||
todo(jon): maybe refactor this a bit to force AppBundle to be created before it can be filled in
|
||||
*/
|
||||
bundle
|
||||
.write_main_executable()
|
||||
.await
|
||||
.context("Failed to write main executable")?;
|
||||
bundle.write_server_executable().await?;
|
||||
bundle
|
||||
.write_assets()
|
||||
.await
|
||||
.context("Failed to write assets")?;
|
||||
bundle.write_metadata().await?;
|
||||
bundle.optimize().await?;
|
||||
bundle.pre_render_ssg_routes().await?;
|
||||
bundle
|
||||
.assemble()
|
||||
.await
|
||||
.context("Failed to assemble app bundle")?;
|
||||
|
||||
tracing::debug!("Bundle created at {}", bundle.build.root_dir().display());
|
||||
|
||||
Ok(bundle)
|
||||
}
|
||||
|
||||
/// Take the output of rustc and make it into the main exe of the bundle
|
||||
///
|
||||
/// For wasm, we'll want to run `wasm-bindgen` to make it a wasm binary along with some other optimizations
|
||||
/// Other platforms we might do some stripping or other optimizations
|
||||
/// Move the executable to the workdir
|
||||
async fn write_main_executable(&mut self) -> Result<()> {
|
||||
match self.build.build.platform() {
|
||||
// Run wasm-bindgen on the wasm binary and set its output to be in the bundle folder
|
||||
// Also run wasm-opt on the wasm binary, and sets the index.html since that's also the "executable".
|
||||
//
|
||||
// The wasm stuff will be in a folder called "wasm" in the workdir.
|
||||
//
|
||||
// Final output format:
|
||||
// ```
|
||||
// dx/
|
||||
// app/
|
||||
// web/
|
||||
// bundle/
|
||||
// build/
|
||||
// public/
|
||||
// index.html
|
||||
// wasm/
|
||||
// app.wasm
|
||||
// glue.js
|
||||
// snippets/
|
||||
// ...
|
||||
// assets/
|
||||
// logo.png
|
||||
// ```
|
||||
Platform::Web => {
|
||||
self.bundle_web().await?;
|
||||
}
|
||||
|
||||
// this will require some extra oomf to get the multi architecture builds...
|
||||
// for now, we just copy the exe into the current arch (which, sorry, is hardcoded for my m1)
|
||||
// we'll want to do multi-arch builds in the future, so there won't be *one* exe dir to worry about
|
||||
// eventually `exe_dir` and `main_exe` will need to take in an arch and return the right exe path
|
||||
//
|
||||
// todo(jon): maybe just symlink this rather than copy it?
|
||||
Platform::Android => {
|
||||
self.copy_android_exe(&self.app.exe, &self.main_exe())
|
||||
.await?;
|
||||
}
|
||||
|
||||
// These are all super simple, just copy the exe into the folder
|
||||
// eventually, perhaps, maybe strip + encrypt the exe?
|
||||
Platform::MacOS
|
||||
| Platform::Windows
|
||||
| Platform::Linux
|
||||
| Platform::Ios
|
||||
| Platform::Liveview
|
||||
| Platform::Server => {
|
||||
std::fs::copy(&self.app.exe, self.main_exe())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy the assets out of the manifest and into the target location
|
||||
///
|
||||
/// Should be the same on all platforms - just copy over the assets from the manifest into the output directory
|
||||
async fn write_assets(&self) -> Result<()> {
|
||||
// Server doesn't need assets - web will provide them
|
||||
if self.build.build.platform() == Platform::Server {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let asset_dir = self.build.asset_dir();
|
||||
|
||||
// First, clear the asset dir of any files that don't exist in the new manifest
|
||||
_ = tokio::fs::create_dir_all(&asset_dir).await;
|
||||
// Create a set of all the paths that new files will be bundled to
|
||||
let mut keep_bundled_output_paths: HashSet<_> = self
|
||||
.app
|
||||
.assets
|
||||
.assets
|
||||
.values()
|
||||
.map(|a| asset_dir.join(a.bundled_path()))
|
||||
.collect();
|
||||
// The CLI creates a .version file in the asset dir to keep track of what version of the optimizer
|
||||
// the asset was processed. If that version doesn't match the CLI version, we need to re-optimize
|
||||
// all assets.
|
||||
let version_file = self.build.asset_optimizer_version_file();
|
||||
let clear_cache = std::fs::read_to_string(&version_file)
|
||||
.ok()
|
||||
.filter(|s| s == crate::VERSION.as_str())
|
||||
.is_none();
|
||||
if clear_cache {
|
||||
keep_bundled_output_paths.clear();
|
||||
}
|
||||
|
||||
// one possible implementation of walking a directory only visiting files
|
||||
fn remove_old_assets<'a>(
|
||||
path: &'a Path,
|
||||
keep_bundled_output_paths: &'a HashSet<PathBuf>,
|
||||
) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
// If this asset is in the manifest, we don't need to remove it
|
||||
let canon_path = dunce::canonicalize(path)?;
|
||||
if keep_bundled_output_paths.contains(canon_path.as_path()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Otherwise, if it is a directory, we need to walk it and remove child files
|
||||
if path.is_dir() {
|
||||
for entry in std::fs::read_dir(path)?.flatten() {
|
||||
let path = entry.path();
|
||||
remove_old_assets(&path, keep_bundled_output_paths).await?;
|
||||
}
|
||||
if path.read_dir()?.next().is_none() {
|
||||
// If the directory is empty, remove it
|
||||
tokio::fs::remove_dir(path).await?;
|
||||
}
|
||||
} else {
|
||||
// If it is a file, remove it
|
||||
tokio::fs::remove_file(path).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
tracing::debug!("Removing old assets");
|
||||
tracing::trace!(
|
||||
"Keeping bundled output paths: {:#?}",
|
||||
keep_bundled_output_paths
|
||||
);
|
||||
remove_old_assets(&asset_dir, &keep_bundled_output_paths).await?;
|
||||
|
||||
// todo(jon): we also want to eventually include options for each asset's optimization and compression, which we currently aren't
|
||||
let mut assets_to_transfer = vec![];
|
||||
|
||||
// Queue the bundled assets
|
||||
for (asset, bundled) in &self.app.assets.assets {
|
||||
let from = asset.clone();
|
||||
let to = asset_dir.join(bundled.bundled_path());
|
||||
|
||||
// prefer to log using a shorter path relative to the workspace dir by trimming the workspace dir
|
||||
let from_ = from
|
||||
.strip_prefix(self.build.krate.workspace_dir())
|
||||
.unwrap_or(from.as_path());
|
||||
let to_ = from
|
||||
.strip_prefix(self.build.krate.workspace_dir())
|
||||
.unwrap_or(to.as_path());
|
||||
|
||||
tracing::debug!("Copying asset {from_:?} to {to_:?}");
|
||||
assets_to_transfer.push((from, to, *bundled.options()));
|
||||
}
|
||||
|
||||
// And then queue the legacy assets
|
||||
// ideally, one day, we can just check the rsx!{} calls for references to assets
|
||||
for from in self.build.krate.legacy_asset_dir_files() {
|
||||
let to = asset_dir.join(from.file_name().unwrap());
|
||||
tracing::debug!("Copying legacy asset {from:?} to {to:?}");
|
||||
assets_to_transfer.push((from, to, AssetOptions::Unknown));
|
||||
}
|
||||
|
||||
let asset_count = assets_to_transfer.len();
|
||||
let started_processing = AtomicUsize::new(0);
|
||||
let copied = AtomicUsize::new(0);
|
||||
|
||||
// Parallel Copy over the assets and keep track of progress with an atomic counter
|
||||
let progress = self.build.progress.clone();
|
||||
let ws_dir = self.build.krate.workspace_dir();
|
||||
// Optimizing assets is expensive and blocking, so we do it in a tokio spawn blocking task
|
||||
tokio::task::spawn_blocking(move || {
|
||||
assets_to_transfer
|
||||
.par_iter()
|
||||
.try_for_each(|(from, to, options)| {
|
||||
let processing = started_processing.fetch_add(1, Ordering::SeqCst);
|
||||
let from_ = from.strip_prefix(&ws_dir).unwrap_or(from);
|
||||
tracing::trace!(
|
||||
"Starting asset copy {processing}/{asset_count} from {from_:?}"
|
||||
);
|
||||
|
||||
let res = process_file_to(options, from, to);
|
||||
if let Err(err) = res.as_ref() {
|
||||
tracing::error!("Failed to copy asset {from:?}: {err}");
|
||||
}
|
||||
|
||||
let finished = copied.fetch_add(1, Ordering::SeqCst);
|
||||
BuildRequest::status_copied_asset(
|
||||
&progress,
|
||||
finished,
|
||||
asset_count,
|
||||
from.to_path_buf(),
|
||||
);
|
||||
|
||||
res.map(|_| ())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("A task failed while trying to copy assets: {e}"))??;
|
||||
|
||||
// // Remove the wasm bindgen output directory if it exists
|
||||
// _ = std::fs::remove_dir_all(self.build.wasm_bindgen_out_dir());
|
||||
|
||||
// Write the version file so we know what version of the optimizer we used
|
||||
std::fs::write(
|
||||
self.build.asset_optimizer_version_file(),
|
||||
crate::VERSION.as_str(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The item that we'll try to run directly if we need to.
|
||||
///
|
||||
/// todo(jon): we should name the app properly instead of making up the exe name. It's kinda okay for dev mode, but def not okay for prod
|
||||
pub fn main_exe(&self) -> PathBuf {
|
||||
self.build.exe_dir().join(self.build.platform_exe_name())
|
||||
}
|
||||
|
||||
/// We always put the server in the `web` folder!
|
||||
/// Only the `web` target will generate a `public` folder though
|
||||
async fn write_server_executable(&self) -> Result<()> {
|
||||
if let Some(server) = &self.server {
|
||||
let to = self
|
||||
.server_exe()
|
||||
.expect("server should be set if we're building a server");
|
||||
|
||||
std::fs::create_dir_all(self.server_exe().unwrap().parent().unwrap())?;
|
||||
|
||||
tracing::debug!("Copying server executable to: {to:?} {server:#?}");
|
||||
|
||||
// Remove the old server executable if it exists, since copying might corrupt it :(
|
||||
// todo(jon): do this in more places, I think
|
||||
_ = std::fs::remove_file(&to);
|
||||
std::fs::copy(&server.exe, to)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// todo(jon): use handlebars templates instead of these prebaked templates
|
||||
async fn write_metadata(&self) -> Result<()> {
|
||||
// write the Info.plist file
|
||||
match self.build.build.platform() {
|
||||
Platform::MacOS => {
|
||||
let dest = self.build.root_dir().join("Contents").join("Info.plist");
|
||||
let plist = self.macos_plist_contents()?;
|
||||
std::fs::write(dest, plist)?;
|
||||
}
|
||||
|
||||
Platform::Ios => {
|
||||
let dest = self.build.root_dir().join("Info.plist");
|
||||
let plist = self.ios_plist_contents()?;
|
||||
std::fs::write(dest, plist)?;
|
||||
}
|
||||
|
||||
// AndroidManifest.xml
|
||||
// er.... maybe even all the kotlin/java/gradle stuff?
|
||||
Platform::Android => {}
|
||||
|
||||
// Probably some custom format or a plist file (haha)
|
||||
// When we do the proper bundle, we'll need to do something with wix templates, I think?
|
||||
Platform::Windows => {}
|
||||
|
||||
// eventually we'll create the .appimage file, I guess?
|
||||
Platform::Linux => {}
|
||||
|
||||
// These are served as folders, not appimages, so we don't need to do anything special (I think?)
|
||||
// Eventually maybe write some secrets/.env files for the server?
|
||||
// We could also distribute them as a deb/rpm for linux and msi for windows
|
||||
Platform::Web => {}
|
||||
Platform::Server => {}
|
||||
Platform::Liveview => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run the optimizers, obfuscators, minimizers, signers, etc
|
||||
pub(crate) async fn optimize(&self) -> Result<()> {
|
||||
match self.build.build.platform() {
|
||||
Platform::Web => {
|
||||
// Compress the asset dir
|
||||
// If pre-compressing is enabled, we can pre_compress the wasm-bindgen output
|
||||
let pre_compress = self
|
||||
.build
|
||||
.krate
|
||||
.should_pre_compress_web_assets(self.build.build.release);
|
||||
|
||||
self.build.status_compressing_assets();
|
||||
let asset_dir = self.build.asset_dir();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
crate::fastfs::pre_compress_folder(&asset_dir, pre_compress)
|
||||
})
|
||||
.await
|
||||
.unwrap()?;
|
||||
}
|
||||
Platform::MacOS => {}
|
||||
Platform::Windows => {}
|
||||
Platform::Linux => {}
|
||||
Platform::Ios => {}
|
||||
Platform::Android => {}
|
||||
Platform::Server => {}
|
||||
Platform::Liveview => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn server_exe(&self) -> Option<PathBuf> {
|
||||
if let Some(_server) = &self.server {
|
||||
let mut path = self
|
||||
.build
|
||||
.krate
|
||||
.build_dir(Platform::Server, self.build.build.release);
|
||||
|
||||
if cfg!(windows) {
|
||||
path.push("server.exe");
|
||||
} else {
|
||||
path.push("server");
|
||||
}
|
||||
|
||||
return Some(path);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Bundle the web app
|
||||
/// - Run wasm-bindgen
|
||||
/// - Bundle split
|
||||
/// - Run wasm-opt
|
||||
/// - Register the .wasm and .js files with the asset system
|
||||
async fn bundle_web(&mut self) -> 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.build.wasm_bindgen_out_dir();
|
||||
let prebindgen = self.app.exe.clone();
|
||||
let post_bindgen_wasm = self.build.wasm_bindgen_wasm_output_file();
|
||||
let should_bundle_split = self.build.build.experimental_wasm_split;
|
||||
let rustc_exe = self.app.exe.with_extension("wasm");
|
||||
let bindgen_version = self
|
||||
.build
|
||||
.krate
|
||||
.wasm_bindgen_version()
|
||||
.expect("this should have been checked by tool verification");
|
||||
|
||||
// Prepare any work dirs
|
||||
std::fs::create_dir_all(&bindgen_outdir)?;
|
||||
|
||||
// Prepare our configuration
|
||||
//
|
||||
// we turn off debug symbols in dev mode but leave them on in release mode (weird!) since
|
||||
// wasm-opt and wasm-split need them to do better optimizations.
|
||||
//
|
||||
// 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 will_wasm_opt = (self.build.build.release || self.build.build.experimental_wasm_split)
|
||||
&& crate::wasm_opt::wasm_opt_available();
|
||||
let keep_debug = self.build.krate.config.web.wasm_opt.debug
|
||||
|| self.build.build.debug_symbols
|
||||
|| self.build.build.experimental_wasm_split
|
||||
|| !self.build.build.release
|
||||
|| will_wasm_opt;
|
||||
let demangle = false;
|
||||
let wasm_opt_options = WasmOptConfig {
|
||||
memory_packing: self.build.build.experimental_wasm_split,
|
||||
debug: self.build.build.debug_symbols,
|
||||
..self.build.krate.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?)
|
||||
self.build.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(&rustc_exe)
|
||||
.target("web")
|
||||
.debug(keep_debug)
|
||||
.demangle(demangle)
|
||||
.keep_debug(keep_debug)
|
||||
.keep_lld_sections(true)
|
||||
.out_name(self.build.krate.executable_name())
|
||||
.out_dir(&bindgen_outdir)
|
||||
.remove_name_section(!will_wasm_opt)
|
||||
.remove_producers_section(!will_wasm_opt)
|
||||
.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 {
|
||||
self.build.status_splitting_bundle();
|
||||
|
||||
if !will_wasm_opt {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Bundle splitting requires wasm-opt to be installed or the CLI to be built with `--features optimizations`. Please install wasm-opt and try again."
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// 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(&prebindgen)?;
|
||||
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(\"/assets/{url}\", [], fusedImports);",
|
||||
url = self
|
||||
.app
|
||||
.assets
|
||||
.register_asset(&path, AssetOptions::Unknown)?.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_{}_{}.wasm", idx, comp_name));
|
||||
wasm_opt::write_wasm(&module.bytes, &path, &wasm_opt_options).await?;
|
||||
|
||||
let hash_id = module.hash_id.as_ref().unwrap();
|
||||
|
||||
writeln!(
|
||||
glue,
|
||||
"export const __wasm_split_load_{module}_{hash_id}_{comp_name} = makeLoad(\"/assets/{url}\", [{deps}], fusedImports);",
|
||||
module = module.module_name,
|
||||
|
||||
|
||||
// Again, register this wasm with the asset system
|
||||
url = self
|
||||
.app
|
||||
.assets
|
||||
.register_asset(&path, AssetOptions::Unknown)?.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::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, glue.as_bytes());
|
||||
std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(self.build.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)?;
|
||||
}
|
||||
|
||||
// Make sure to optimize the main wasm file if requested or if bundle splitting
|
||||
if should_bundle_split || self.build.build.release {
|
||||
self.build.status_optimizing_wasm();
|
||||
wasm_opt::optimize(&post_bindgen_wasm, &post_bindgen_wasm, &wasm_opt_options).await?;
|
||||
}
|
||||
|
||||
// Make sure to register the main wasm file with the asset system
|
||||
self.app
|
||||
.assets
|
||||
.register_asset(&post_bindgen_wasm, AssetOptions::Unknown)?;
|
||||
|
||||
// Register the main.js with the asset system so it bundles in the snippets and optimizes
|
||||
self.app.assets.register_asset(
|
||||
&self.build.wasm_bindgen_js_output_file(),
|
||||
AssetOptions::Js(JsAssetOptions::new().with_minify(true).with_preload(true)),
|
||||
)?;
|
||||
|
||||
// Write the index.html file with the pre-configured contents we got from pre-rendering
|
||||
std::fs::write(
|
||||
self.build.root_dir().join("index.html"),
|
||||
self.prepare_html()?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn pre_render_ssg_routes(&self) -> Result<()> {
|
||||
// Run SSG and cache static routes
|
||||
if !self.build.build.ssg {
|
||||
return Ok(());
|
||||
}
|
||||
self.build.status_prerendering_routes();
|
||||
pre_render_static_routes(
|
||||
&self
|
||||
.server_exe()
|
||||
.context("Failed to find server executable")?,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn macos_plist_contents(&self) -> Result<String> {
|
||||
handlebars::Handlebars::new()
|
||||
.render_template(
|
||||
include_str!("../../assets/macos/mac.plist.hbs"),
|
||||
&InfoPlistData {
|
||||
display_name: self.build.krate.bundled_app_name(),
|
||||
bundle_name: self.build.krate.bundled_app_name(),
|
||||
executable_name: self.build.platform_exe_name(),
|
||||
bundle_identifier: self.build.krate.bundle_identifier(),
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn ios_plist_contents(&self) -> Result<String> {
|
||||
handlebars::Handlebars::new()
|
||||
.render_template(
|
||||
include_str!("../../assets/ios/ios.plist.hbs"),
|
||||
&InfoPlistData {
|
||||
display_name: self.build.krate.bundled_app_name(),
|
||||
bundle_name: self.build.krate.bundled_app_name(),
|
||||
executable_name: self.build.platform_exe_name(),
|
||||
bundle_identifier: self.build.krate.bundle_identifier(),
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Run any final tools to produce apks or other artifacts we might need.
|
||||
async fn assemble(&self) -> Result<()> {
|
||||
if let Platform::Android = self.build.build.platform() {
|
||||
self.build.status_running_gradle();
|
||||
|
||||
let output = Command::new(self.gradle_exe()?)
|
||||
.arg("assembleDebug")
|
||||
.current_dir(self.build.root_dir())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::anyhow!("Failed to assemble apk: {output:?}").into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run bundleRelease and return the path to the `.aab` file
|
||||
///
|
||||
/// https://stackoverflow.com/questions/57072558/whats-the-difference-between-gradlewassemblerelease-gradlewinstallrelease-and
|
||||
pub(crate) async fn android_gradle_bundle(&self) -> Result<PathBuf> {
|
||||
let output = Command::new(self.gradle_exe()?)
|
||||
.arg("bundleRelease")
|
||||
.current_dir(self.build.root_dir())
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to run gradle bundleRelease")?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::anyhow!("Failed to bundleRelease: {output:?}").into());
|
||||
}
|
||||
|
||||
let app_release = self
|
||||
.build
|
||||
.root_dir()
|
||||
.join("app")
|
||||
.join("build")
|
||||
.join("outputs")
|
||||
.join("bundle")
|
||||
.join("release");
|
||||
|
||||
// Rename it to Name-arch.aab
|
||||
let from = app_release.join("app-release.aab");
|
||||
let to = app_release.join(format!(
|
||||
"{}-{}.aab",
|
||||
self.build.krate.bundled_app_name(),
|
||||
self.build.build.target_args.arch()
|
||||
));
|
||||
|
||||
std::fs::rename(from, &to).context("Failed to rename aab")?;
|
||||
|
||||
Ok(to)
|
||||
}
|
||||
|
||||
fn gradle_exe(&self) -> Result<PathBuf> {
|
||||
// make sure we can execute the gradlew script
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
std::fs::set_permissions(
|
||||
self.build.root_dir().join("gradlew"),
|
||||
std::fs::Permissions::from_mode(0o755),
|
||||
)?;
|
||||
}
|
||||
|
||||
let gradle_exec_name = match cfg!(windows) {
|
||||
true => "gradlew.bat",
|
||||
false => "gradlew",
|
||||
};
|
||||
|
||||
Ok(self.build.root_dir().join(gradle_exec_name))
|
||||
}
|
||||
|
||||
pub(crate) fn apk_path(&self) -> PathBuf {
|
||||
self.build
|
||||
.root_dir()
|
||||
.join("app")
|
||||
.join("build")
|
||||
.join("outputs")
|
||||
.join("apk")
|
||||
.join("debug")
|
||||
.join("app-debug.apk")
|
||||
}
|
||||
|
||||
/// Copy the Android executable to the target directory, and rename the hardcoded com_hardcoded_dioxuslabs entries
|
||||
/// to the user's app name.
|
||||
async fn copy_android_exe(&self, source: &Path, destination: &Path) -> Result<()> {
|
||||
// we might want to eventually use the objcopy logic to handle this
|
||||
//
|
||||
// https://github.com/rust-mobile/xbuild/blob/master/xbuild/template/lib.rs
|
||||
// https://github.com/rust-mobile/xbuild/blob/master/apk/src/lib.rs#L19
|
||||
std::fs::copy(source, destination)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
//! Report progress about the build to the user. We use channels to report progress back to the CLI.
|
||||
|
||||
use super::BuildMode;
|
||||
use crate::{BuildArtifacts, BuildStage, Error, TraceSrc};
|
||||
use cargo_metadata::CompilerMessage;
|
||||
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, process::ExitStatus};
|
||||
|
||||
/// 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
|
||||
/// and the BuildMode can change while serving.
|
||||
///
|
||||
/// The structure of this is roughly taken from cargo itself which uses a similar pattern.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BuildContext {
|
||||
pub tx: ProgressTx,
|
||||
pub mode: BuildMode,
|
||||
}
|
||||
|
||||
pub type ProgressTx = UnboundedSender<BuilderUpdate>;
|
||||
pub type ProgressRx = UnboundedReceiver<BuilderUpdate>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct BuildId(pub(crate) usize);
|
||||
impl BuildId {
|
||||
pub const CLIENT: Self = Self(0);
|
||||
pub const SERVER: Self = Self(1);
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum BuilderUpdate {
|
||||
Progress {
|
||||
stage: BuildStage,
|
||||
},
|
||||
|
||||
CompilerMessage {
|
||||
message: CompilerMessage,
|
||||
},
|
||||
|
||||
BuildReady {
|
||||
bundle: BuildArtifacts,
|
||||
},
|
||||
|
||||
BuildFailed {
|
||||
err: Error,
|
||||
},
|
||||
|
||||
/// A running process has received a stdout.
|
||||
/// May or may not be a complete line - do not treat it as a line. It will include a line if it is a complete line.
|
||||
///
|
||||
/// We will poll lines and any content in a 50ms interval
|
||||
StdoutReceived {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// A running process has received a stderr.
|
||||
/// May or may not be a complete line - do not treat it as a line. It will include a line if it is a complete line.
|
||||
///
|
||||
/// We will poll lines and any content in a 50ms interval
|
||||
StderrReceived {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
ProcessExited {
|
||||
status: ExitStatus,
|
||||
},
|
||||
}
|
||||
|
||||
impl BuildContext {
|
||||
pub(crate) fn status_wasm_bindgen_start(&self) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::RunningBindgen,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_splitting_bundle(&self) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::SplittingBundle,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_start_bundle(&self) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::Bundling,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_running_gradle(&self) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::RunningGradle,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn status_build_diagnostic(&self, message: CompilerMessage) {
|
||||
_ = self
|
||||
.tx
|
||||
.unbounded_send(BuilderUpdate::CompilerMessage { message });
|
||||
}
|
||||
|
||||
pub(crate) fn status_build_error(&self, line: String) {
|
||||
tracing::error!(dx_src = ?TraceSrc::Cargo, "{line}");
|
||||
}
|
||||
|
||||
pub(crate) fn status_build_message(&self, line: String) {
|
||||
tracing::trace!(dx_src = ?TraceSrc::Cargo, "{line}");
|
||||
}
|
||||
|
||||
pub(crate) fn status_build_progress(&self, count: usize, total: usize, name: String) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::Compiling {
|
||||
current: count,
|
||||
total,
|
||||
krate: name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_starting_build(&self, crate_count: usize) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::Starting {
|
||||
patch: matches!(self.mode, BuildMode::Thin { .. }),
|
||||
crate_count,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_starting_link(&self) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::Linking,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_copied_asset(
|
||||
progress: &UnboundedSender<BuilderUpdate>,
|
||||
current: usize,
|
||||
total: usize,
|
||||
path: PathBuf,
|
||||
) {
|
||||
_ = progress.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::CopyingAssets {
|
||||
current,
|
||||
total,
|
||||
path,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_optimizing_wasm(&self) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::OptimizingWasm,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_hotpatching(&self) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::Hotpatching,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_installing_tooling(&self) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::InstallingTooling,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_compressing_assets(&self) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::CompressingAssets,
|
||||
});
|
||||
}
|
||||
pub(crate) fn status_extracting_assets(&self) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::ExtractingAssets,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
//! The primary entrypoint for our build + optimize + bundle engine
|
||||
//! The core build module for `dx`, enabling building, bundling, and runtime hot-patching of Rust
|
||||
//! applications. This module defines the entire end-to-end build process, including bundling for
|
||||
//! all major platforms including Mac, Windows, Linux, iOS, Android, and WebAssembly.
|
||||
//!
|
||||
//! Handles multiple ongoing tasks and allows you to queue up builds from interactive and non-interactive contexts
|
||||
//!
|
||||
//! Uses a request -> response architecture that allows you to monitor the progress with an optional message
|
||||
//! receiver.
|
||||
//! The bulk of the builder code is contained within the [`request`] module which establishes the
|
||||
//! arguments and flow of the build process. The [`context`] module contains the context for the build
|
||||
//! including status updates and build customization. The [`patch`] module contains the logic for
|
||||
//! 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.
|
||||
|
||||
mod builder;
|
||||
mod bundle;
|
||||
mod prerender;
|
||||
mod progress;
|
||||
mod context;
|
||||
mod patch;
|
||||
mod request;
|
||||
mod templates;
|
||||
mod verify;
|
||||
mod web;
|
||||
mod tools;
|
||||
|
||||
pub(crate) use builder::*;
|
||||
pub(crate) use bundle::*;
|
||||
pub(crate) use progress::*;
|
||||
pub(crate) use context::*;
|
||||
pub(crate) use patch::*;
|
||||
pub(crate) use request::*;
|
||||
pub(crate) use tools::*;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,125 +0,0 @@
|
||||
use anyhow::Context;
|
||||
use dioxus_cli_config::{server_ip, server_port};
|
||||
use futures_util::stream::FuturesUnordered;
|
||||
use futures_util::StreamExt;
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
path::Path,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::process::Command;
|
||||
|
||||
pub(crate) async fn pre_render_static_routes(server_exe: &Path) -> anyhow::Result<()> {
|
||||
// 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 moe them into multiple tasks below
|
||||
let address = &address;
|
||||
let port = &port;
|
||||
|
||||
tracing::info!("Running SSG at http://{address}:{port} for {server_exe:?}");
|
||||
|
||||
// Run the server executable
|
||||
let _child = Command::new(server_exe)
|
||||
.env(dioxus_cli_config::SERVER_PORT_ENV, port)
|
||||
.env(dioxus_cli_config::SERVER_IP_ENV, address)
|
||||
.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,120 +0,0 @@
|
||||
//! Report progress about the build to the user. We use channels to report progress back to the CLI.
|
||||
use crate::{AppBundle, BuildRequest, BuildStage, Platform, TraceSrc};
|
||||
use cargo_metadata::CompilerMessage;
|
||||
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub(crate) type ProgressTx = UnboundedSender<BuildUpdate>;
|
||||
pub(crate) type ProgressRx = UnboundedReceiver<BuildUpdate>;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum BuildUpdate {
|
||||
Progress { stage: BuildStage },
|
||||
CompilerMessage { message: CompilerMessage },
|
||||
BuildReady { bundle: AppBundle },
|
||||
BuildFailed { err: crate::Error },
|
||||
}
|
||||
|
||||
impl BuildRequest {
|
||||
pub(crate) fn status_wasm_bindgen_start(&self) {
|
||||
_ = self.progress.unbounded_send(BuildUpdate::Progress {
|
||||
stage: BuildStage::RunningBindgen {},
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_splitting_bundle(&self) {
|
||||
_ = self.progress.unbounded_send(BuildUpdate::Progress {
|
||||
stage: BuildStage::SplittingBundle,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_start_bundle(&self) {
|
||||
_ = self.progress.unbounded_send(BuildUpdate::Progress {
|
||||
stage: BuildStage::Bundling {},
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_running_gradle(&self) {
|
||||
_ = self.progress.unbounded_send(BuildUpdate::Progress {
|
||||
stage: BuildStage::RunningGradle,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn status_build_diagnostic(&self, message: CompilerMessage) {
|
||||
_ = self
|
||||
.progress
|
||||
.unbounded_send(BuildUpdate::CompilerMessage { message });
|
||||
}
|
||||
|
||||
pub(crate) fn status_build_error(&self, line: String) {
|
||||
tracing::error!(dx_src = ?TraceSrc::Cargo, "{line}");
|
||||
}
|
||||
|
||||
pub(crate) fn status_build_message(&self, line: String) {
|
||||
tracing::trace!(dx_src = ?TraceSrc::Cargo, "{line}");
|
||||
}
|
||||
|
||||
pub(crate) fn status_build_progress(&self, count: usize, total: usize, name: String) {
|
||||
_ = self.progress.unbounded_send(BuildUpdate::Progress {
|
||||
stage: BuildStage::Compiling {
|
||||
current: count,
|
||||
total,
|
||||
krate: name,
|
||||
is_server: self.is_server(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_starting_build(&self, crate_count: usize) {
|
||||
_ = self.progress.unbounded_send(BuildUpdate::Progress {
|
||||
stage: BuildStage::Starting {
|
||||
is_server: self.build.platform() == Platform::Server,
|
||||
crate_count,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_copied_asset(
|
||||
progress: &UnboundedSender<BuildUpdate>,
|
||||
current: usize,
|
||||
total: usize,
|
||||
path: PathBuf,
|
||||
) {
|
||||
_ = progress.unbounded_send(BuildUpdate::Progress {
|
||||
stage: BuildStage::CopyingAssets {
|
||||
current,
|
||||
total,
|
||||
path,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_optimizing_wasm(&self) {
|
||||
_ = self.progress.unbounded_send(BuildUpdate::Progress {
|
||||
stage: BuildStage::OptimizingWasm {},
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_prerendering_routes(&self) {
|
||||
_ = self.progress.unbounded_send(BuildUpdate::Progress {
|
||||
stage: BuildStage::PrerenderingRoutes {},
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_installing_tooling(&self) {
|
||||
_ = self.progress.unbounded_send(BuildUpdate::Progress {
|
||||
stage: BuildStage::InstallingTooling {},
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn status_compressing_assets(&self) {
|
||||
_ = self.progress.unbounded_send(BuildUpdate::Progress {
|
||||
stage: BuildStage::CompressingAssets,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn is_server(&self) -> bool {
|
||||
self.build.platform() == Platform::Server
|
||||
}
|
||||
}
|
||||
+3327
-566
File diff suppressed because it is too large.
Load diff
@@ -1,7 +0,0 @@
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct InfoPlistData {
|
||||
pub display_name: String,
|
||||
pub bundle_name: String,
|
||||
pub bundle_identifier: String,
|
||||
pub executable_name: String,
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
use itertools::Itertools;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use target_lexicon::{
|
||||
Aarch64Architecture, Architecture, ArmArchitecture, Environment, OperatingSystem, 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) 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,
|
||||
}))
|
||||
}
|
||||
|
||||
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) -> PathBuf {
|
||||
let suffix = if cfg!(target_os = "windows") {
|
||||
".cmd"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
self.android_tools_dir().join(format!(
|
||||
"{}{}-clang{}",
|
||||
triple,
|
||||
self.min_sdk_version(),
|
||||
suffix
|
||||
))
|
||||
}
|
||||
|
||||
// todo(jon): this should be configurable
|
||||
pub(crate) fn min_sdk_version(&self) -> u32 {
|
||||
24
|
||||
}
|
||||
|
||||
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();
|
||||
triple.operating_system = OperatingSystem::Linux;
|
||||
triple.environment = Environment::Android;
|
||||
triple.architecture = target_lexicon::HOST.architecture;
|
||||
|
||||
// TODO: Wire this up with --device flag. (add `-s serial`` flag before `shell` arg)
|
||||
let output = Command::new(&self.adb)
|
||||
.arg("shell")
|
||||
.arg("uname")
|
||||
.arg("-m")
|
||||
.output()
|
||||
.await
|
||||
.map(|out| String::from_utf8(out.stdout));
|
||||
|
||||
match output {
|
||||
Ok(Ok(out)) => match out.trim() {
|
||||
"armv7l" => 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;
|
||||
}
|
||||
other => {
|
||||
tracing::warn!("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
|
||||
}
|
||||
}
|
||||
|
||||
fn var_or_debug(name: &str) -> Option<PathBuf> {
|
||||
use std::env::var;
|
||||
use tracing::debug;
|
||||
|
||||
var(name)
|
||||
.inspect_err(|_| debug!("{name} not set"))
|
||||
.ok()
|
||||
.map(PathBuf::from)
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
use crate::{wasm_bindgen::WasmBindgen, BuildRequest, Error, Platform, Result, RustcDetails};
|
||||
use anyhow::{anyhow, Context};
|
||||
|
||||
impl BuildRequest {
|
||||
/// Check for tooling that might be required for this build.
|
||||
///
|
||||
/// This should generally be only called on the first build since it takes time to verify the tooling
|
||||
/// is in place, and we don't want to slow down subsequent builds.
|
||||
pub(crate) async fn verify_tooling(&self) -> Result<()> {
|
||||
tracing::debug!("Verifying tooling...");
|
||||
self.status_installing_tooling();
|
||||
|
||||
self.krate
|
||||
.initialize_profiles()
|
||||
.context("Failed to initialize profiles - dioxus can't build without them. You might need to initialize them yourself.")?;
|
||||
|
||||
let rustc = match RustcDetails::from_cli().await {
|
||||
Ok(out) => out,
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to verify tooling: {err}\ndx will proceed, but you might run into errors later.");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
match self.build.platform() {
|
||||
Platform::Web => self.verify_web_tooling(rustc).await?,
|
||||
Platform::Ios => self.verify_ios_tooling(rustc).await?,
|
||||
Platform::Android => self.verify_android_tooling(rustc).await?,
|
||||
Platform::Linux => self.verify_linux_tooling(rustc).await?,
|
||||
Platform::MacOS => {}
|
||||
Platform::Windows => {}
|
||||
Platform::Server => {}
|
||||
Platform::Liveview => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn verify_web_tooling(&self, rustc: RustcDetails) -> Result<()> {
|
||||
// Install target using rustup.
|
||||
#[cfg(not(feature = "no-downloads"))]
|
||||
if !rustc.has_wasm32_unknown_unknown() {
|
||||
tracing::info!(
|
||||
"Web platform requires wasm32-unknown-unknown to be installed. Installing..."
|
||||
);
|
||||
|
||||
let _ = tokio::process::Command::new("rustup")
|
||||
.args(["target", "add", "wasm32-unknown-unknown"])
|
||||
.output()
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Ensure target is installed.
|
||||
if !rustc.has_wasm32_unknown_unknown() {
|
||||
return Err(Error::Other(anyhow!(
|
||||
"Missing target wasm32-unknown-unknown."
|
||||
)));
|
||||
}
|
||||
|
||||
// Wasm bindgen
|
||||
let krate_bindgen_version = self.krate.wasm_bindgen_version().ok_or(anyhow!(
|
||||
"failed to detect wasm-bindgen version, unable to proceed"
|
||||
))?;
|
||||
|
||||
WasmBindgen::verify_install(&krate_bindgen_version).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Currently does nothing, but eventually we need to check that the mobile tooling is installed.
|
||||
///
|
||||
/// For ios, this would be just aarch64-apple-ios + aarch64-apple-ios-sim, as well as xcrun and xcode-select
|
||||
///
|
||||
/// We don't auto-install these yet since we're not doing an architecture check. We assume most users
|
||||
/// are running on an Apple Silicon Mac, but it would be confusing if we installed these when we actually
|
||||
/// should be installing the x86 versions.
|
||||
pub(crate) async fn verify_ios_tooling(&self, _rustc: RustcDetails) -> Result<()> {
|
||||
// open the simulator
|
||||
// _ = tokio::process::Command::new("open")
|
||||
// .arg("/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app")
|
||||
// .stderr(Stdio::piped())
|
||||
// .stdout(Stdio::piped())
|
||||
// .status()
|
||||
// .await;
|
||||
|
||||
// Now xcrun to open the device
|
||||
// todo: we should try and query the device list and/or parse it rather than hardcode this simulator
|
||||
// _ = tokio::process::Command::new("xcrun")
|
||||
// .args(["simctl", "boot", "83AE3067-987F-4F85-AE3D-7079EF48C967"])
|
||||
// .stderr(Stdio::piped())
|
||||
// .stdout(Stdio::piped())
|
||||
// .status()
|
||||
// .await;
|
||||
|
||||
// if !rustup
|
||||
// .installed_toolchains
|
||||
// .contains(&"aarch64-apple-ios".to_string())
|
||||
// {
|
||||
// tracing::error!("You need to install aarch64-apple-ios to build for ios. Run `rustup target add aarch64-apple-ios` to install it.");
|
||||
// }
|
||||
|
||||
// if !rustup
|
||||
// .installed_toolchains
|
||||
// .contains(&"aarch64-apple-ios-sim".to_string())
|
||||
// {
|
||||
// tracing::error!("You need to install aarch64-apple-ios to build for ios. Run `rustup target add aarch64-apple-ios` to install it.");
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the android tooling is installed
|
||||
///
|
||||
/// looks for the android sdk + ndk
|
||||
///
|
||||
/// will do its best to fill in the missing bits by exploring the sdk structure
|
||||
/// IE will attempt to use the Java installed from android studio if possible.
|
||||
pub(crate) async fn verify_android_tooling(&self, _rustc: RustcDetails) -> Result<()> {
|
||||
let result = self
|
||||
.krate
|
||||
.android_ndk()
|
||||
.map(|ndk| self.build.target_args.arch().android_linker(&ndk));
|
||||
|
||||
if let Some(path) = result {
|
||||
if path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!(
|
||||
"Android linker not found. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation."
|
||||
).into())
|
||||
}
|
||||
|
||||
/// Ensure the right dependencies are installed for linux apps.
|
||||
/// This varies by distro, so we just do nothing for now.
|
||||
///
|
||||
/// Eventually, we want to check for the prereqs for wry/tao as outlined by tauri:
|
||||
/// https://tauri.app/start/prerequisites/
|
||||
pub(crate) async fn verify_linux_tooling(&self, _rustc: RustcDetails) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
use dioxus_cli_config::format_base_path_meta_element;
|
||||
use manganis::AssetOptions;
|
||||
|
||||
use crate::error::Result;
|
||||
use std::fmt::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::AppBundle;
|
||||
|
||||
const DEFAULT_HTML: &str = include_str!("../../assets/web/index.html");
|
||||
const TOAST_HTML: &str = include_str!("../../assets/web/toast.html");
|
||||
|
||||
impl AppBundle {
|
||||
pub(crate) fn prepare_html(&self) -> Result<String> {
|
||||
let mut html = {
|
||||
let crate_root: &Path = &self.build.krate.crate_dir();
|
||||
let custom_html_file = crate_root.join("index.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(&mut html)?;
|
||||
|
||||
// Inject loading scripts if they are not already present
|
||||
self.inject_loading_scripts(&mut html);
|
||||
|
||||
// Replace any special placeholders in the HTML with resolved values
|
||||
self.replace_template_placeholders(&mut html);
|
||||
|
||||
let title = self.build.krate.config.web.app.title.clone();
|
||||
|
||||
replace_or_insert_before("{app_title}", "</title", &title, &mut html);
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
fn is_dev_build(&self) -> bool {
|
||||
!self.build.build.release
|
||||
}
|
||||
|
||||
// Inject any resources from the config into the html
|
||||
fn inject_resources(&self, html: &mut String) -> Result<()> {
|
||||
// Collect all resources into a list of styles and scripts
|
||||
let resources = &self.build.krate.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.build.krate.config.web.app.base_path {
|
||||
head_resources.push_str(&format_base_path_meta_element(base_path));
|
||||
}
|
||||
}
|
||||
|
||||
if !style_list.is_empty() {
|
||||
self.send_resource_deprecation_warning(style_list, ResourceType::Style);
|
||||
}
|
||||
if !script_list.is_empty() {
|
||||
self.send_resource_deprecation_warning(script_list, ResourceType::Script);
|
||||
}
|
||||
|
||||
// Inject any resources from manganis into the head
|
||||
for asset in self.app.assets.assets.values() {
|
||||
let asset_path = asset.bundled_path();
|
||||
match asset.options() {
|
||||
AssetOptions::Css(css_options) => {
|
||||
if css_options.preloaded() {
|
||||
head_resources.push_str(&format!(
|
||||
"<link rel=\"preload\" as=\"style\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
|
||||
))
|
||||
}
|
||||
}
|
||||
AssetOptions::Image(image_options) => {
|
||||
if image_options.preloaded() {
|
||||
head_resources.push_str(&format!(
|
||||
"<link rel=\"preload\" as=\"image\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
|
||||
))
|
||||
}
|
||||
}
|
||||
AssetOptions::Js(js_options) => {
|
||||
if js_options.preloaded() {
|
||||
head_resources.push_str(&format!(
|
||||
"<link rel=\"preload\" as=\"script\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Manually inject the wasm file for preloading. WASM currently doesn't support preloading in the manganis asset system
|
||||
let wasm_source_path = self.build.wasm_bindgen_wasm_output_file();
|
||||
let wasm_path = self
|
||||
.app
|
||||
.assets
|
||||
.assets
|
||||
.get(&wasm_source_path)
|
||||
.expect("WASM asset should exist in web bundles")
|
||||
.bundled_path();
|
||||
head_resources.push_str(&format!(
|
||||
"<link rel=\"preload\" as=\"fetch\" type=\"application/wasm\" href=\"/{{base_path}}/assets/{wasm_path}\" crossorigin>"
|
||||
));
|
||||
|
||||
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, html: &mut String) {
|
||||
// If it looks like we are already loading wasm or the current build opted out of injecting loading scripts, don't inject anything
|
||||
if !self.build.build.inject_loading_scripts || html.contains("__wbindgen_start") {
|
||||
return;
|
||||
}
|
||||
|
||||
// If not, insert the script
|
||||
*html = html.replace(
|
||||
"</body",
|
||||
r#" <script>
|
||||
// We can't use a module script here because we need to start the script immediately when streaming
|
||||
import("/{base_path}/{js_path}").then(
|
||||
({ default: init, initSync }) => {
|
||||
// export initSync in case a split module needs to initialize
|
||||
window.__wasm_split_main_initSync = initSync;
|
||||
|
||||
// Actually perform the load
|
||||
init({module_or_path: "/{base_path}/{wasm_path}"}).then((wasm) => {
|
||||
if (wasm.__wbindgen_start == undefined) {
|
||||
wasm.main();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
</script>
|
||||
{DX_TOAST_UTILITIES}
|
||||
</body"#,
|
||||
);
|
||||
|
||||
// Trim out the toasts if we're in release, or add them if we're serving
|
||||
*html = match self.is_dev_build() {
|
||||
true => html.replace("{DX_TOAST_UTILITIES}", TOAST_HTML),
|
||||
false => html.replace("{DX_TOAST_UTILITIES}", ""),
|
||||
};
|
||||
}
|
||||
|
||||
/// Replace any special placeholders in the HTML with resolved values
|
||||
fn replace_template_placeholders(&self, html: &mut String) {
|
||||
let base_path = self.build.krate.config.web.app.base_path();
|
||||
*html = html.replace("{base_path}", base_path);
|
||||
|
||||
let app_name = &self.build.krate.executable_name();
|
||||
let wasm_source_path = self.build.wasm_bindgen_wasm_output_file();
|
||||
let wasm_path = self
|
||||
.app
|
||||
.assets
|
||||
.assets
|
||||
.get(&wasm_source_path)
|
||||
.expect("WASM asset should exist in web bundles")
|
||||
.bundled_path();
|
||||
let wasm_path = format!("assets/{wasm_path}");
|
||||
let js_source_path = self.build.wasm_bindgen_js_output_file();
|
||||
let js_path = self
|
||||
.app
|
||||
.assets
|
||||
.assets
|
||||
.get(&js_source_path)
|
||||
.expect("JS asset should exist in web bundles")
|
||||
.bundled_path();
|
||||
let js_path = format!("assets/{js_path}");
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
fn send_resource_deprecation_warning(&self, paths: Vec<PathBuf>, variant: ResourceType) {
|
||||
const RESOURCE_DEPRECATION_MESSAGE: &str = r#"The `web.resource` config has been deprecated in favor of head components and will be removed in a future release. Instead of including assets in the config, you can include assets with the `asset!` macro and add them to the head with `document::Link` and `Script` components."#;
|
||||
|
||||
let replacement_components = paths
|
||||
.iter()
|
||||
.map(|path| {
|
||||
let path = if path.exists() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
// If the path is absolute, make it relative to the current directory before we join it
|
||||
// The path is actually a web path which is relative to the root of the website
|
||||
let path = path.strip_prefix("/").unwrap_or(path);
|
||||
let asset_dir_path = self
|
||||
.build
|
||||
.krate
|
||||
.legacy_asset_dir()
|
||||
.map(|dir| dir.join(path).canonicalize());
|
||||
|
||||
if let Some(Ok(absolute_path)) = asset_dir_path {
|
||||
let absolute_crate_root =
|
||||
self.build.krate.crate_dir().canonicalize().unwrap();
|
||||
PathBuf::from("./")
|
||||
.join(absolute_path.strip_prefix(absolute_crate_root).unwrap())
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
};
|
||||
match variant {
|
||||
ResourceType::Style => {
|
||||
format!(" Stylesheet {{ href: asset!(\"{}\") }}", path.display())
|
||||
}
|
||||
ResourceType::Script => {
|
||||
format!(" Script {{ src: asset!(\"{}\") }}", path.display())
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let replacement_components = format!("rsx! {{\n{}\n}}", replacement_components.join("\n"));
|
||||
let section_name = match variant {
|
||||
ResourceType::Style => "web.resource.style",
|
||||
ResourceType::Script => "web.resource.script",
|
||||
};
|
||||
|
||||
tracing::warn!(
|
||||
"{RESOURCE_DEPRECATION_MESSAGE}\nTo migrate to head components, remove `{section_name}` and include the following rsx in your root component:\n```rust\n{replacement_components}\n```"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ResourceType {
|
||||
Style,
|
||||
Script,
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user