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:
Jonathan Kelley authored and GitHub committed 2025-05-06 23:08:59 -07:00
1 parent 71d85e6dd5
commit d976ca184a
142 files changed
+17982 -7156

No files matched your search

File diff suppressed because it is too large. Load diff
-955
View File
@@ -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(())
}
}
+177
View File
@@ -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,
});
}
}
+14 -13
View File
@@ -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
-125
View File
@@ -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(())
}
-120
View File
@@ -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
}
}
File diff suppressed because it is too large. Load diff
-7
View File
@@ -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,
}
+257
View File
@@ -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)
}
-143
View File
@@ -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(())
}
}
-272
View File
@@ -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);
}
}