cli json output, dx bundle fix, dx serve --platform android, race condition, drop ssg platform (#3186)

* clean up logging to avoid random extra trace

* fix race condition with updates

* properly wire up verbose

* fix bundling (macos) and logging

* Add structured output

* clean up clap names

* extract out wire format

* switch structured key to json

* fix random println, fallback to `dioxus/platform`

* clean up logging for run/build

* clean up logging around project

* remove manual exits

* fix tokio runtime for mobile

* rework dog app

* rip out ssg

* Switch dioxus/axum to dioxus/server

* add android template inline

* pre restructure for bundle prep

* add the whole res directory

* Better theme for the app

* remove mobile demo now that most apps work natively

* self-referential android

* only use deep linking for assets

* fix imports for android

* clippy, fixup ios and android

* I'm not boxing compiler message you can't make me

* fix clippy on unix
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2024-11-12 09:01:01 -05:00
1 parent 8a998e7d49
commit ac3e33af46
116 files changed
+2367 -6091

No files matched your search

+311
View File
@@ -0,0 +1,311 @@
use crate::{
AppBundle, BuildArgs, BuildRequest, BuildStage, BuildUpdate, DioxusCrate, ProgressRx,
ProgressTx, Result, StructuredOutput,
};
use std::time::{Duration, Instant};
/// The component of the serve engine that watches ongoing builds and manages their state, handle,
/// and progress.
///
/// Previously, the builder allowed multiple apps to be built simultaneously, but this newer design
/// simplifies the code and allows only one app and its server to be built at a time.
///
/// Here, we track the number of crates being compiled, assets copied, the times of these events, and
/// other metadata that gives us useful indicators for the UI.
pub(crate) struct Builder {
// Components of the build
pub krate: DioxusCrate,
pub request: BuildRequest,
pub build: tokio::task::JoinHandle<Result<AppBundle>>,
pub tx: ProgressTx,
pub rx: ProgressRx,
// Metadata about the build that needs to be managed by watching build updates
// used to render the TUI
pub stage: BuildStage,
pub compiled_crates: usize,
pub compiled_crates_server: usize,
pub expected_crates: usize,
pub expected_crates_server: usize,
pub bundling_progress: f64,
pub compile_start: Option<Instant>,
pub compile_end: Option<Instant>,
pub compile_end_server: Option<Instant>,
pub bundle_start: Option<Instant>,
pub bundle_end: Option<Instant>,
}
impl Builder {
/// Create a new builder and immediately start a build
pub(crate) fn start(krate: &DioxusCrate, args: BuildArgs) -> Result<Self> {
let (tx, rx) = futures_channel::mpsc::unbounded();
let request = BuildRequest::new(krate.clone(), args, tx.clone());
Ok(Self {
krate: krate.clone(),
request: request.clone(),
stage: BuildStage::Initializing,
build: tokio::spawn(async move {
// On the first build, we want to verify the tooling
// We wont bother verifying on subsequent builds
request.verify_tooling().await?;
request.build_all().await
}),
tx,
rx,
compiled_crates: 0,
expected_crates: 1,
expected_crates_server: 1,
compiled_crates_server: 0,
bundling_progress: 0.0,
compile_start: Some(Instant::now()),
compile_end: None,
compile_end_server: None,
bundle_start: None,
bundle_end: None,
})
}
/// Wait for any new updates to the builder - either it completed or gave us a message etc
pub(crate) async fn wait(&mut self) -> BuildUpdate {
use futures_util::StreamExt;
// Wait for the build to finish or for it to emit a status message
let update = tokio::select! {
Some(progress) = self.rx.next() => progress,
bundle = (&mut self.build) => {
// Replace the build with an infinitely pending task so we can select it again without worrying about deadlocks/spins
self.build = tokio::task::spawn(std::future::pending());
match bundle {
Ok(Ok(bundle)) => BuildUpdate::BuildReady { bundle },
Ok(Err(err)) => BuildUpdate::BuildFailed { err },
Err(err) => BuildUpdate::BuildFailed { err: crate::Error::Runtime(format!("Build panicked! {:?}", err)) },
}
},
};
// Update the internal stage of the build so the UI can render it
match &update {
BuildUpdate::Progress { stage } => {
// Prevent updates from flowing in after the build has already finished
if !self.is_finished() {
self.stage = stage.clone();
match stage {
BuildStage::Initializing => {
self.compiled_crates = 0;
self.compiled_crates_server = 0;
self.bundling_progress = 0.0;
}
BuildStage::Starting {
crate_count,
is_server,
} => {
if *is_server {
self.expected_crates_server = *crate_count;
} else {
self.expected_crates = *crate_count;
}
}
BuildStage::InstallingTooling {} => {}
BuildStage::Compiling {
current,
total,
is_server,
..
} => {
if *is_server {
self.compiled_crates_server = *current;
self.expected_crates_server = *total;
} else {
self.compiled_crates = *current;
self.expected_crates = *total;
}
if self.compile_start.is_none() {
self.compile_start = Some(Instant::now());
}
}
BuildStage::Bundling {} => {
self.complete_compile();
self.bundling_progress = 0.0;
self.bundle_start = Some(Instant::now());
}
BuildStage::OptimizingWasm {} => {}
BuildStage::CopyingAssets { current, total, .. } => {
self.bundling_progress = *current as f64 / *total as f64;
}
BuildStage::Success => {
self.compiled_crates = self.expected_crates;
self.compiled_crates_server = self.expected_crates_server;
self.bundling_progress = 1.0;
}
BuildStage::Failed => {
self.compiled_crates = self.expected_crates;
self.compiled_crates_server = self.expected_crates_server;
self.bundling_progress = 1.0;
}
BuildStage::Aborted => {}
BuildStage::Restarting => {
self.compiled_crates = 0;
self.compiled_crates_server = 0;
self.expected_crates = 1;
self.bundling_progress = 0.0;
}
BuildStage::RunningBindgen {} => {}
_ => {}
}
}
}
BuildUpdate::CompilerMessage { .. } => {}
BuildUpdate::BuildReady { .. } => {
self.compiled_crates = self.expected_crates;
self.compiled_crates_server = self.expected_crates_server;
self.bundling_progress = 1.0;
self.stage = BuildStage::Success;
self.complete_compile();
self.bundle_end = Some(Instant::now());
}
BuildUpdate::BuildFailed { .. } => {
tracing::debug!("Setting builder to failed state");
self.stage = BuildStage::Failed;
}
}
update
}
/// Restart this builder with new build arguments.
pub(crate) fn rebuild(&mut self, args: BuildArgs) {
// Abort all the ongoing builds, cleaning up any loose artifacts and waiting to cleanly exit
self.abort_all();
// And then start a new build, resetting our progress/stage to the beginning and replacing the old tokio task
let request = BuildRequest::new(self.krate.clone(), args, self.tx.clone());
self.request = request.clone();
self.stage = BuildStage::Restarting;
// This build doesn't have any extra special logging - rebuilds would get pretty noisy
self.build = tokio::spawn(async move { request.build_all().await });
}
/// Shutdown the current build process
///
/// todo: might want to use a cancellation token here to allow cleaner shutdowns
pub(crate) fn abort_all(&mut self) {
self.build.abort();
self.stage = BuildStage::Aborted;
self.compiled_crates = 0;
self.compiled_crates_server = 0;
self.expected_crates = 1;
self.bundling_progress = 0.0;
self.compile_start = None;
self.bundle_start = None;
self.bundle_end = None;
self.compile_end = None;
}
/// Wait for the build to finish, returning the final bundle
/// Should only be used by code that's not interested in the intermediate updates and only cares about the final bundle
///
/// todo(jon): maybe we want to do some logging here? The build/bundle/run screens could be made to
/// use the TUI output for prettier outputs.
pub(crate) async fn finish(&mut self) -> Result<AppBundle> {
loop {
match self.wait().await {
BuildUpdate::Progress { stage } => {
match &stage {
BuildStage::Compiling {
current,
total,
krate,
..
} => {
tracing::info!("Compiling [{current:>3}/{total}]: {krate}");
}
BuildStage::RunningBindgen => tracing::info!("Running wasm-bindgen..."),
BuildStage::CopyingAssets {
current,
total,
path,
} => {
tracing::info!("Copying asset ({current}/{total}): {}", path.display());
}
BuildStage::Bundling => tracing::info!("Bundling app..."),
_ => {}
}
tracing::info!(json = ?StructuredOutput::BuildUpdate { stage: stage.clone() });
}
BuildUpdate::CompilerMessage { message } => {
tracing::info!(json = ?StructuredOutput::CargoOutput { message: message.clone() }, %message);
}
BuildUpdate::BuildReady { bundle } => {
tracing::debug!(json = ?StructuredOutput::BuildFinished {
path: bundle.build.root_dir(),
});
return Ok(bundle);
}
BuildUpdate::BuildFailed { err } => {
tracing::error!(?err, json = ?StructuredOutput::Error { message: err.to_string() });
return Err(err);
}
}
}
}
fn complete_compile(&mut self) {
if self.compile_end.is_none() {
self.compiled_crates = self.expected_crates;
self.compile_end = Some(Instant::now());
self.compile_end_server = Some(Instant::now());
}
}
/// Get the total duration of the build, if all stages have completed
pub(crate) fn total_build_time(&self) -> Option<Duration> {
Some(self.compile_duration()? + self.bundle_duration()?)
}
pub(crate) fn compile_duration(&self) -> Option<Duration> {
Some(
self.compile_end
.unwrap_or_else(Instant::now)
.duration_since(self.compile_start?),
)
}
pub(crate) fn bundle_duration(&self) -> Option<Duration> {
Some(
self.bundle_end
.unwrap_or_else(Instant::now)
.duration_since(self.bundle_start?),
)
}
/// Return a number between 0 and 1 representing the progress of the app build
pub(crate) fn compile_progress(&self) -> f64 {
self.compiled_crates as f64 / self.expected_crates as f64
}
/// Return a number between 0 and 1 representing the progress of the server build
pub(crate) fn server_compile_progress(&self) -> f64 {
self.compiled_crates_server as f64 / self.expected_crates_server as f64
}
pub(crate) fn bundle_progress(&self) -> f64 {
self.bundling_progress
}
pub(crate) fn is_finished(&self) -> bool {
match self.stage {
BuildStage::Success => true,
BuildStage::Failed => true,
BuildStage::Aborted => true,
BuildStage::Restarting => false,
_ => false,
}
}
}
+777
View File
@@ -0,0 +1,777 @@
use crate::Result;
use crate::{assets::AssetManifest, TraceSrc};
use crate::{BuildRequest, Platform};
use anyhow::Context;
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use std::{
fs::create_dir_all,
path::{Path, PathBuf},
};
use std::{sync::atomic::AtomicUsize, time::Duration};
use tokio::process::Command;
use wasm_bindgen_cli_support::Bindgen;
use super::templates::InfoPlistData;
/// 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 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
.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(&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 => {
// Run wasm-bindgen and drop its output into the assets folder under "dioxus"
self.build.status_wasm_bindgen_start();
self.run_wasm_bindgen(&self.app.exe.with_extension("wasm"), &self.build.exe_dir())
.await?;
// Only run wasm-opt if the feature is enabled
// Wasm-opt has an expensive build script that makes it annoying to keep enabled for iterative dev
// We put it behind the "wasm-opt" feature flag so that it can be disabled when iterating on the cli
self.run_wasm_opt(&self.build.exe_dir())?;
// 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.build.prepare_html()?,
)?;
}
// 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 => {
// 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(&self.app.exe, self.main_exe())?;
}
// 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
// todo(jon): cache the asset dir, removing old files and only copying new ones that changed since the last build
_ = std::fs::remove_dir_all(&asset_dir);
_ = create_dir_all(&asset_dir);
// 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 in self.app.assets.assets.keys() {
let bundled = self.app.assets.assets.get(asset).unwrap();
let from = bundled.absolute.clone();
let to = asset_dir.join(&bundled.bundled);
tracing::debug!("Copying asset {from:?} to {to:?}");
assets_to_transfer.push((from, to));
}
// 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));
}
let asset_count = assets_to_transfer.len();
let assets_finished = AtomicUsize::new(0);
// Parallel Copy over the assets and keep track of progress with an atomic counter
// todo: we want to use the fastfs variant that knows how to parallelize folders, too
assets_to_transfer.par_iter().try_for_each(|(from, to)| {
tracing::trace!(
"Starting asset copy {current}/{asset_count} from {from:?}",
current = assets_finished.fetch_add(0, std::sync::atomic::Ordering::SeqCst),
);
// todo(jon): implement optimize + pre_compress on the asset type
let res = crate::fastfs::copy_asset(from, to);
if let Err(err) = res.as_ref() {
tracing::error!("Failed to copy asset {from:?}: {err}");
}
self.build.status_copied_asset(
assets_finished.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1,
asset_count,
from.clone(),
);
res.map(|_| ())
})?;
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 from {server:?} to {to:?}");
// 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);
let bindgen_dir = self.build.exe_dir();
tokio::task::spawn_blocking(move || {
crate::fastfs::pre_compress_folder(&bindgen_dir, pre_compress)
})
.await
.unwrap()?;
// Run SSG and cache static routes
if self.build.build.ssg {
self.run_ssg().await?;
}
}
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
}
pub(crate) async fn run_wasm_bindgen(
&self,
input_path: &Path,
bindgen_outdir: &Path,
) -> anyhow::Result<()> {
tracing::debug!(dx_src = ?TraceSrc::Bundle, "Running wasm-bindgen");
let input_path = input_path.to_path_buf();
let bindgen_outdir = bindgen_outdir.to_path_buf();
let name = self.build.krate.executable_name().to_string();
let keep_debug = self.build.krate.config.web.wasm_opt.debug;
let reference_types = self.build.krate.config.web.wasm_opt.reference_types;
let start = std::time::Instant::now();
tokio::task::spawn_blocking(move || {
Bindgen::new()
.input_path(&input_path)
.web(true)
.unwrap()
.debug(keep_debug)
.demangle(keep_debug)
.keep_debug(keep_debug)
.reference_types(reference_types)
.remove_name_section(true)
.remove_producers_section(true)
.out_name(&name)
.generate(&bindgen_outdir)
})
.await
.context("Wasm-bindgen crashed while optimizing the wasm binary")?
.context("Failed to generate wasm-bindgen bindings")?;
tracing::debug!(dx_src = ?TraceSrc::Bundle, "wasm-bindgen complete in {:?}", start.elapsed());
Ok(())
}
#[allow(unused)]
pub(crate) fn run_wasm_opt(&self, bindgen_outdir: &std::path::Path) -> Result<()> {
if !self.build.build.release {
return Ok(());
};
self.build.status_optimizing_wasm();
#[cfg(feature = "optimizations")]
{
use crate::config::WasmOptLevel;
tracing::info!(dx_src = ?TraceSrc::Build, "Running optimization with wasm-opt...");
let mut options = match self.build.krate.config.web.wasm_opt.level {
WasmOptLevel::Z => {
wasm_opt::OptimizationOptions::new_optimize_for_size_aggressively()
}
WasmOptLevel::S => wasm_opt::OptimizationOptions::new_optimize_for_size(),
WasmOptLevel::Zero => wasm_opt::OptimizationOptions::new_opt_level_0(),
WasmOptLevel::One => wasm_opt::OptimizationOptions::new_opt_level_1(),
WasmOptLevel::Two => wasm_opt::OptimizationOptions::new_opt_level_2(),
WasmOptLevel::Three => wasm_opt::OptimizationOptions::new_opt_level_3(),
WasmOptLevel::Four => wasm_opt::OptimizationOptions::new_opt_level_4(),
};
let wasm_file =
bindgen_outdir.join(format!("{}_bg.wasm", self.build.krate.executable_name()));
let old_size = wasm_file.metadata()?.len();
options
// WASM bindgen relies on reference types
.enable_feature(wasm_opt::Feature::ReferenceTypes)
.debug_info(self.build.krate.config.web.wasm_opt.debug)
.run(&wasm_file, &wasm_file)
.map_err(|err| crate::Error::Other(anyhow::anyhow!(err)))?;
let new_size = wasm_file.metadata()?.len();
tracing::debug!(
dx_src = ?TraceSrc::Build,
"wasm-opt reduced WASM size from {} to {} ({:2}%)",
old_size,
new_size,
(new_size as f64 - old_size as f64) / old_size as f64 * 100.0
);
}
Ok(())
}
async fn run_ssg(&self) -> anyhow::Result<()> {
use futures_util::stream::FuturesUnordered;
use futures_util::StreamExt;
use tokio::process::Command;
const PORT: u16 = 9999;
tracing::info!("Running SSG");
// Run the server executable
let _child = Command::new(
self.server_exe()
.context("Failed to find server executable")?,
)
.env(dioxus_cli_config::SERVER_PORT_ENV, PORT.to_string())
.env(dioxus_cli_config::SERVER_IP_ENV, "127.0.0.1")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()?;
// Wait a second for the server to start
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
// Get the routes from the `/static_routes` endpoint
let mut routes = reqwest::Client::builder()
.build()?
.post(format!("http://127.0.0.1:{PORT}/api/static_routes"))
.send()
.await
.context("Failed to get static routes from server")?
.text()
.await
.map(|raw| serde_json::from_str::<String>(&raw).unwrap())
.inspect(|text| tracing::debug!("Got static routes: {text:?}"))
.context("Failed to parse static routes from server")?
.lines()
.map(|line| line.to_string())
.map(|line| async move {
tracing::info!("SSG: {line}");
reqwest::Client::builder()
.build()?
.get(format!("http://127.0.0.1:{PORT}{line}"))
.header("Accept", "text/html")
.send()
.await
})
.collect::<FuturesUnordered<_>>();
while let Some(route) = routes.next().await {
match route {
Ok(route) => tracing::debug!("ssg success: {route:?}"),
Err(err) => tracing::error!("ssg error: {err:?}"),
}
}
// Wait a second for the cache to be written by the server
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
tracing::info!("SSG complete");
drop(_child);
Ok(())
}
pub(crate) fn bundle_identifier(&self) -> String {
format!("com.dioxuslabs.{}", self.build.krate.executable_name())
}
fn macos_plist_contents(&self) -> Result<String> {
handlebars::Handlebars::new()
.render_template(
include_str!("../../assets/macos/mac.plist.hbs"),
&InfoPlistData {
display_name: self.build.platform_exe_name(),
bundle_name: self.build.platform_exe_name(),
executable_name: self.build.platform_exe_name(),
bundle_identifier: format!("com.dioxuslabs.{}", self.build.platform_exe_name()),
},
)
.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.platform_exe_name(),
bundle_name: self.build.platform_exe_name(),
executable_name: self.build.platform_exe_name(),
bundle_identifier: format!("com.dioxuslabs.{}", self.build.platform_exe_name()),
},
)
.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() {
// 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 output = Command::new("./gradlew")
.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(())
}
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")
}
}
+19
View File
@@ -0,0 +1,19 @@
//! The primary entrypoint for our build + optimize + bundle engine
//!
//! 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.
mod builder;
mod bundle;
mod progress;
mod request;
mod templates;
mod verify;
mod web;
pub(crate) use builder::*;
pub(crate) use bundle::*;
pub(crate) use progress::*;
pub(crate) use request::*;
+87
View File
@@ -0,0 +1,87 @@
//! 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_start_bundle(&self) {
_ = self.progress.unbounded_send(BuildUpdate::Progress {
stage: BuildStage::Bundling {},
});
}
pub(crate) fn status_build_diagnostic(&self, message: CompilerMessage) {
_ = self
.progress
.unbounded_send(BuildUpdate::CompilerMessage { message });
}
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(&self, current: usize, total: usize, path: PathBuf) {
_ = self.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_installing_tooling(&self) {
_ = self.progress.unbounded_send(BuildUpdate::Progress {
stage: BuildStage::InstallingTooling {},
});
}
pub(crate) fn is_server(&self) -> bool {
self.build.platform() == Platform::Server
}
}
+786
View File
@@ -0,0 +1,786 @@
use super::{progress::ProgressTx, BuildArtifacts};
use crate::dioxus_crate::DioxusCrate;
use crate::Result;
use crate::{assets::AssetManifest, TraceSrc};
use crate::{link::LinkAction, BuildArgs};
use crate::{AppBundle, Platform};
use anyhow::Context;
use serde::Deserialize;
use std::{path::PathBuf, process::Stdio, time::Instant};
use tokio::{io::AsyncBufReadExt, process::Command};
#[derive(Clone, Debug)]
pub(crate) struct BuildRequest {
/// The configuration for the crate we are building
pub(crate) krate: DioxusCrate,
/// The arguments for the build
pub(crate) build: BuildArgs,
/// Status channel to send our progress updates to
pub(crate) progress: ProgressTx,
/// The target directory for the build
pub(crate) custom_target_dir: Option<PathBuf>,
}
impl BuildRequest {
pub fn new(krate: DioxusCrate, build: BuildArgs, progress: ProgressTx) -> Self {
Self {
build,
krate,
progress,
custom_target_dir: None,
}
}
/// Run the build command with a pretty loader, returning the executable output location
///
/// This will also run the fullstack build. Note that fullstack is handled separately within this
/// code flow rather than outside of it.
pub(crate) async fn build_all(self) -> Result<AppBundle> {
tracing::debug!("Running build command...");
let (app, server) = self.build_concurrent().await?;
AppBundle::new(self, app, server).await
}
/// Run the build command with a pretty loader, returning the executable output location
async fn build_concurrent(&self) -> Result<(BuildArtifacts, Option<BuildArtifacts>)> {
let (app, server) =
futures_util::future::try_join(self.build_app(), self.build_server()).await?;
Ok((app, server))
}
pub(crate) async fn build_app(&self) -> Result<BuildArtifacts> {
tracing::debug!("Building app...");
let start = Instant::now();
self.prepare_build_dir()?;
let exe = self.build_cargo().await?;
let assets = self.collect_assets().await?;
Ok(BuildArtifacts {
exe,
assets,
time_taken: start.elapsed(),
})
}
pub(crate) async fn build_server(&self) -> Result<Option<BuildArtifacts>> {
tracing::debug!("Building server...");
if !self.build.fullstack {
return Ok(None);
}
let mut cloned = self.clone();
cloned.build.platform = Some(Platform::Server);
Ok(Some(cloned.build_app().await?))
}
/// Run `cargo`, returning the location of the final executable
///
/// todo: add some stats here, like timing reports, crate-graph optimizations, etc
pub(crate) async fn build_cargo(&self) -> Result<PathBuf> {
tracing::debug!("Executing cargo...");
// Extract the unit count of the crate graph so build_cargo has more accurate data
let crate_count = self.get_unit_count_estimate().await;
// Update the status to show that we're starting the build and how many crates we expect to build
self.status_starting_build(crate_count);
let mut cmd = Command::new("cargo");
cmd.arg("rustc")
.current_dir(self.krate.crate_dir())
.arg("--message-format")
.arg("json-diagnostic-rendered-ansi")
.args(self.build_arguments())
.envs(self.env_vars());
// .args(["--", "-Csave-temps=y"]);
if let Some(target_dir) = self.custom_target_dir.as_ref() {
cmd.env("CARGO_TARGET_DIR", target_dir);
}
// Android needs a special linker since the linker is actually tied to the android toolchain.
// For the sake of simplicity, we're going to pass the linker here using ourselves as the linker,
// but in reality we could simply use the android toolchain's linker as the path.
//
// We don't want to overwrite the user's .cargo/config.toml since that gets committed to git
// and we want everyone's install to be the same.
if self.build.platform() == Platform::Android {
cmd.env(
LinkAction::ENV_VAR_NAME,
LinkAction::LinkAndroid {
linker: "/Users/jonkelley/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang".into(),
extra_flags: vec![],
}
.to_json(),
);
}
tracing::trace!(dx_src = ?TraceSrc::Build, "Rust cargo args: {:?}", cmd);
let mut child = cmd
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("Failed to spawn cargo build")?;
let stdout = tokio::io::BufReader::new(child.stdout.take().unwrap());
let stderr = tokio::io::BufReader::new(child.stderr.take().unwrap());
let mut output_location = None;
let mut stdout = stdout.lines();
let mut stderr = stderr.lines();
let mut units_compiled = 0;
loop {
use cargo_metadata::Message;
let line = tokio::select! {
Ok(Some(line)) = stdout.next_line() => line,
Ok(Some(line)) = stderr.next_line() => line,
else => break,
};
let mut deserializer = serde_json::Deserializer::from_str(line.trim());
deserializer.disable_recursion_limit();
let message =
Message::deserialize(&mut deserializer).unwrap_or(Message::TextLine(line));
match message {
Message::BuildScriptExecuted(_) => units_compiled += 1,
Message::TextLine(line) => self.status_build_message(line),
Message::CompilerMessage(msg) => self.status_build_diagnostic(msg),
Message::CompilerArtifact(artifact) => {
units_compiled += 1;
match artifact.executable {
Some(executable) => output_location = Some(executable.into()),
None => self.status_build_progress(
units_compiled,
crate_count,
artifact.target.name,
),
}
}
Message::BuildFinished(finished) => {
if !finished.success {
return Err(anyhow::anyhow!(
"Cargo build failed, signaled by the compiler"
)
.into());
}
}
_ => {}
}
}
if output_location.is_none() {
tracing::error!("Cargo build failed - no output location");
}
let out_location = output_location.context("Build did not return an executable")?;
tracing::debug!(
"Build completed successfully - output location: {:?}",
out_location
);
Ok(out_location)
}
/// Run the linker intercept and then fill in our AssetManifest from the incremental artifacts
///
/// This will execute `dx` with an env var set to force `dx` to operate as a linker, and then
/// traverse the .o and .rlib files rustc passes that new `dx` instance, collecting the link
/// tables marked by manganis and parsing them as a ResourceAsset.
pub(crate) async fn collect_assets(&self) -> Result<AssetManifest> {
tracing::debug!("Collecting assets ...");
if self.build.skip_assets {
return Ok(AssetManifest::default());
}
self.deep_linker_asset_extract().await
}
/// Create a list of arguments for cargo builds
pub(crate) fn build_arguments(&self) -> Vec<String> {
let mut cargo_args = Vec::new();
// Set the target, profile and features that vary between the app and server builds
if self.build.platform() == Platform::Server {
cargo_args.push("--profile".to_string());
match self.build.release {
true => cargo_args.push("release".to_string()),
false => cargo_args.push(self.build.server_profile.to_string()),
};
} else {
// Add required profile flags. --release overrides any custom profiles.
let custom_profile = &self.build.profile.as_ref();
if custom_profile.is_some() || self.build.release {
cargo_args.push("--profile".to_string());
match self.build.release {
true => cargo_args.push("release".to_string()),
false => {
cargo_args.push(
custom_profile
.expect("custom_profile should have been checked by is_some")
.to_string(),
);
}
};
}
// todo: use the right arch based on the current arch
let custom_target = match self.build.platform() {
Platform::Web => Some("wasm32-unknown-unknown"),
Platform::Ios => match self.build.target_args.device {
Some(true) => Some("aarch64-apple-ios"),
_ => Some("aarch64-apple-ios-sim"),
},
Platform::Android => Some("aarch64-linux-android"),
Platform::Server => None,
// we're assuming we're building for the native platform for now... if you're cross-compiling
// the targets here might be different
Platform::MacOS => None,
Platform::Windows => None,
Platform::Linux => None,
Platform::Liveview => None,
};
if let Some(target) = custom_target.or(self.build.target_args.target.as_deref()) {
cargo_args.push("--target".to_string());
cargo_args.push(target.to_string());
}
}
// We always run in verbose since the CLI itself is the one doing the presentation
cargo_args.push("--verbose".to_string());
if self.build.target_args.no_default_features {
cargo_args.push("--no-default-features".to_string());
}
let features = self.target_features();
if !features.is_empty() {
cargo_args.push("--features".to_string());
cargo_args.push(features.join(" "));
}
if let Some(ref package) = self.build.target_args.package {
cargo_args.push(String::from("-p"));
cargo_args.push(package.clone());
}
cargo_args.append(&mut self.build.cargo_args.clone());
match self.krate.executable_type() {
krates::cm::TargetKind::Bin => cargo_args.push("--bin".to_string()),
krates::cm::TargetKind::Lib => cargo_args.push("--lib".to_string()),
krates::cm::TargetKind::Example => cargo_args.push("--example".to_string()),
_ => {}
};
cargo_args.push(self.krate.executable_name().to_string());
tracing::debug!(dx_src = ?TraceSrc::Build, "cargo args: {:?}", cargo_args);
cargo_args
}
#[allow(dead_code)]
pub(crate) fn android_rust_flags(&self) -> String {
let mut rust_flags = std::env::var("RUSTFLAGS").unwrap_or_default();
if self.build.platform() == Platform::Android {
let cur_exe = std::env::current_exe().unwrap();
rust_flags.push_str(format!(" -Clinker={}", cur_exe.display()).as_str());
rust_flags.push_str(" -Clink-arg=-landroid");
rust_flags.push_str(" -Clink-arg=-llog");
rust_flags.push_str(" -Clink-arg=-lOpenSLES");
rust_flags.push_str(" -Clink-arg=-Wl,--export-dynamic");
}
rust_flags
}
/// Create the list of features we need to pass to cargo to build the app by merging together
/// either the client or server features depending on if we're building a server or not.
pub(crate) fn target_features(&self) -> Vec<String> {
let mut features = self.build.target_args.features.clone();
if self.build.platform() == Platform::Server {
features.extend(self.build.target_args.server_features.clone());
} else {
features.extend(self.build.target_args.client_features.clone());
}
features
}
pub(crate) fn all_target_features(&self) -> Vec<String> {
let mut features = self.target_features();
if !self.build.target_args.no_default_features {
features.extend(
self.krate
.package()
.features
.get("default")
.cloned()
.unwrap_or_default(),
);
}
features.dedup();
features
}
/// Try to get the unit graph for the crate. This is a nightly only feature which may not be available with the current version of rustc the user has installed.
pub(crate) async fn get_unit_count(&self) -> crate::Result<usize> {
#[derive(Debug, Deserialize)]
struct UnitGraph {
units: Vec<serde_json::Value>,
}
let output = tokio::process::Command::new("cargo")
.arg("+nightly")
.arg("build")
.arg("--unit-graph")
.arg("-Z")
.arg("unstable-options")
.args(self.build_arguments())
.envs(self.env_vars())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await?;
if !output.status.success() {
return Err(anyhow::anyhow!("Failed to get unit count").into());
}
let output_text = String::from_utf8(output.stdout).context("Failed to get unit count")?;
let graph: UnitGraph =
serde_json::from_str(&output_text).context("Failed to get unit count")?;
Ok(graph.units.len())
}
/// Get an estimate of the number of units in the crate. If nightly rustc is not available, this will return an estimate of the number of units in the crate based on cargo metadata.
/// TODO: always use https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#unit-graph once it is stable
pub(crate) async fn get_unit_count_estimate(&self) -> usize {
// Try to get it from nightly
self.get_unit_count().await.unwrap_or_else(|_| {
// Otherwise, use cargo metadata
(self
.krate
.krates
.krates_filtered(krates::DepKind::Dev)
.iter()
.map(|k| k.targets.len())
.sum::<usize>() as f64
/ 3.5) as usize
})
}
/// We used to require traversing incremental artifacts for assets that were included but not
/// directly exposed to the final binary. Now, however, we force APIs to carry items created
/// from asset calls into top-level items such that they *do* get included in the final binary.
///
/// There's a chance that's not actually true, so this function is kept around in case we do
/// need to revert to "deep extraction".
async fn deep_linker_asset_extract(&self) -> Result<AssetManifest> {
// Create a temp file to put the output of the args
// We need to do this since rustc won't actually print the link args to stdout, so we need to
// give `dx` a file to dump its env::args into
let tmp_file = tempfile::NamedTempFile::new()?;
// Run `cargo rustc` again, but this time with a custom linker (dx) and an env var to force
// `dx` to act as a linker
//
// This will force `dx` to look through the incremental cache and find the assets from the previous build
Command::new("cargo")
.arg("rustc")
.args(self.build_arguments())
.envs(self.env_vars())
.arg("--offline") /* don't use the network, should already be resolved */
.arg("--")
.arg(format!(
"-Clinker={}",
std::env::current_exe()
.unwrap()
.canonicalize()
.unwrap()
.display()
))
.env(
LinkAction::ENV_VAR_NAME,
LinkAction::BuildAssetManifest {
destination: tmp_file.path().to_path_buf().clone(),
}
.to_json(),
)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await?;
// The linker wrote the manifest to the temp file, let's load it!
let manifest = AssetManifest::load_from_file(tmp_file.path())?;
if let Ok(path) = std::env::var("DEEPLINK").map(|s| s.parse::<PathBuf>().unwrap()) {
_ = tmp_file.persist(path);
}
Ok(manifest)
}
fn env_vars(&self) -> Vec<(&str, String)> {
let mut env_vars = vec![];
if self.build.platform() == Platform::Android {
let app = self.root_dir().join("app");
let app_main = app.join("src").join("main");
let app_kotlin = app_main.join("kotlin");
let app_kotlin_out = app_kotlin.join("com").join("example").join("androidfinal");
env_vars.push((
"WRY_ANDROID_PACKAGE",
"com.example.androidfinal".to_string(),
));
env_vars.push(("WRY_ANDROID_LIBRARY", "androidfinal".to_string()));
env_vars.push((
"WRY_ANDROID_KOTLIN_FILES_OUT_DIR",
app_kotlin_out.display().to_string(),
));
env_vars.push(("RUSTFLAGS", self.android_rust_flags()))
};
env_vars
}
/// We only really currently care about:
///
/// - app dir (.app, .exe, .apk, etc)
/// - assets dir
/// - exe dir (.exe, .app, .apk, etc)
/// - extra scaffolding
///
/// It's not guaranteed that they're different from any other folder
fn prepare_build_dir(&self) -> Result<()> {
use once_cell::sync::OnceCell;
use std::fs::{create_dir_all, remove_dir_all};
static INITIALIZED: OnceCell<Result<()>> = OnceCell::new();
let success = INITIALIZED.get_or_init(|| {
_ = remove_dir_all(self.root_dir());
create_dir_all(self.root_dir())?;
create_dir_all(self.exe_dir())?;
create_dir_all(self.asset_dir())?;
tracing::debug!("Initialized Root dir: {:?}", self.root_dir());
tracing::debug!("Initialized Exe dir: {:?}", self.exe_dir());
tracing::debug!("Initialized Asset dir: {:?}", self.asset_dir());
// we could download the templates from somewhere (github?) but after having banged my head against
// cargo-mobile2 for ages, I give up with that. We're literally just going to hardcode the templates
// by writing them here.
if let Platform::Android = self.build.platform() {
self.build_android_app_dir()?;
}
Ok(())
});
if let Err(e) = success.as_ref() {
return Err(format!("Failed to initialize build directory: {e}").into());
}
Ok(())
}
/// The directory in which we'll put the main exe
///
/// Mac, Android, Web are a little weird
/// - mac wants to be in Contents/MacOS
/// - android wants to be in jniLibs/arm64-v8a (or others, depending on the platform / architecture)
/// - web wants to be in wasm (which... we don't really need to, we could just drop the wasm into public and it would work)
///
/// I think all others are just in the root folder
///
/// todo(jon): investigate if we need to put .wasm in `wasm`. It kinda leaks implementation details, which ideally we don't want to do.
pub fn exe_dir(&self) -> PathBuf {
match self.build.platform() {
Platform::MacOS => self.root_dir().join("Contents").join("MacOS"),
Platform::Web => self.root_dir().join("wasm"),
// Android has a whole build structure to it
Platform::Android => self
.root_dir()
.join("app")
.join("src")
.join("main")
.join("jniLibs")
.join("arm64-v8a"),
// these are all the same, I think?
Platform::Windows
| Platform::Linux
| Platform::Ios
| Platform::Server
| Platform::Liveview => self.root_dir(),
}
}
/// returns the path to root build folder. This will be our working directory for the build.
///
/// we only add an extension to the folders where it sorta matters that it's named with the extension.
/// for example, on mac, the `.app` indicates we can `open` it and it pulls in icons, dylibs, etc.
///
/// for our simulator-based platforms, this is less important since they need to be zipped up anyways
/// to run in the simulator.
///
/// For windows/linux, it's also not important since we're just running the exe directly out of the folder
///
/// The idea of this folder is that we can run our top-level build command against it and we'll get
/// a final build output somewhere. Some platforms have basically no build command, and can simply
/// be ran by executing the exe directly.
pub(crate) fn root_dir(&self) -> PathBuf {
let platform_dir = self
.krate
.build_dir(self.build.platform(), self.build.release);
match self.build.platform() {
Platform::Web => platform_dir.join("public"),
Platform::Server => platform_dir.clone(), // ends up *next* to the public folder
// These might not actually need to be called `.app` but it does let us run these with `open`
Platform::MacOS => platform_dir.join(format!("{}.app", self.platform_exe_name())),
Platform::Ios => platform_dir.join(format!("{}.app", self.platform_exe_name())),
// in theory, these all could end up directly in the root dir
Platform::Android => platform_dir.join("app"), // .apk (after bundling)
Platform::Linux => platform_dir.join("app"), // .appimage (after bundling)
Platform::Windows => platform_dir.join("app"), // .exe (after bundling)
Platform::Liveview => platform_dir.join("app"), // .exe (after bundling)
}
}
pub fn asset_dir(&self) -> PathBuf {
match self.build.platform() {
Platform::MacOS => self
.root_dir()
.join("Contents")
.join("Resources")
.join("assets"),
Platform::Android => self
.root_dir()
.join("app")
.join("src")
.join("main")
.join("assets"),
// everyone else is soooo normal, just app/assets :)
Platform::Web
| Platform::Ios
| Platform::Windows
| Platform::Linux
| Platform::Server
| Platform::Liveview => self.root_dir().join("assets"),
}
}
pub fn platform_exe_name(&self) -> String {
match self.build.platform() {
Platform::MacOS => self.krate.executable_name().to_string(),
Platform::Ios => self.krate.executable_name().to_string(),
Platform::Server => self.krate.executable_name().to_string(),
Platform::Liveview => self.krate.executable_name().to_string(),
Platform::Windows => format!("{}.exe", self.krate.executable_name()),
// from the apk spec, the root exe will actually be a shared library
Platform::Android => format!("lib{}.so", self.krate.executable_name()),
Platform::Web => unimplemented!("there's no main exe on web"), // this will be wrong, I think, but not important?
// todo: maybe this should be called AppRun?
Platform::Linux => self.krate.executable_name().to_string(),
}
}
fn build_android_app_dir(&self) -> Result<()> {
use std::fs::{create_dir_all, write};
let root = self.root_dir();
// gradle
let wrapper = root.join("gradle").join("wrapper");
create_dir_all(&wrapper)?;
tracing::debug!("Initialized Gradle wrapper: {:?}", wrapper);
// app
let app = root.join("app");
let app_main = app.join("src").join("main");
let app_kotlin = app_main.join("kotlin");
let app_jnilibs = app_main.join("jniLibs");
let app_assets = app_main.join("assets");
let app_kotlin_out = app_kotlin.join("com").join("example").join("androidfinal");
create_dir_all(&app)?;
create_dir_all(&app_main)?;
create_dir_all(&app_kotlin)?;
create_dir_all(&app_jnilibs)?;
create_dir_all(&app_assets)?;
create_dir_all(&app_kotlin_out)?;
tracing::debug!("Initialized app: {:?}", app);
tracing::debug!("Initialized app/src: {:?}", app_main);
tracing::debug!("Initialized app/src/kotlin: {:?}", app_kotlin);
tracing::debug!("Initialized app/src/jniLibs: {:?}", app_jnilibs);
tracing::debug!("Initialized app/src/assets: {:?}", app_assets);
tracing::debug!("Initialized app/src/kotlin/main: {:?}", app_kotlin_out);
// Top-level gradle config
write(
root.join("build.gradle.kts"),
include_bytes!("../../assets/android/gen/build.gradle.kts"),
)?;
write(
root.join("gradle.properties"),
include_bytes!("../../assets/android/gen/gradle.properties"),
)?;
write(
root.join("gradlew"),
include_bytes!("../../assets/android/gen/gradlew"),
)?;
write(
root.join("gradlew.bat"),
include_bytes!("../../assets/android/gen/gradlew.bat"),
)?;
write(
root.join("settings.gradle"),
include_bytes!("../../assets/android/gen/settings.gradle"),
)?;
// Then the wrapper and its properties
write(
wrapper.join("gradle-wrapper.properties"),
include_bytes!("../../assets/android/gen/gradle/wrapper/gradle-wrapper.properties"),
)?;
write(
wrapper.join("gradle-wrapper.jar"),
include_bytes!("../../assets/android/gen/gradle/wrapper/gradle-wrapper.jar"),
)?;
// Now the app directory
write(
app.join("build.gradle.kts"),
include_bytes!("../../assets/android/gen/app/build.gradle.kts"),
)?;
write(
app.join("proguard-rules.pro"),
include_bytes!("../../assets/android/gen/app/proguard-rules.pro"),
)?;
write(
app.join("src").join("main").join("AndroidManifest.xml"),
include_bytes!("../../assets/android/gen/app/src/main/AndroidManifest.xml"),
)?;
// Write the main activity manually since tao dropped support for it
write(
app_main
.join("kotlin")
.join("com")
.join("example")
.join("androidfinal")
.join("MainActivity.kt"),
include_bytes!("../../assets/android/MainActivity.kt"),
)?;
// Write the res folder
let res = app_main.join("res");
create_dir_all(&res)?;
create_dir_all(res.join("values"))?;
write(
res.join("values").join("strings.xml"),
include_bytes!("../../assets/android/gen/app/src/main/res/values/strings.xml"),
)?;
write(
res.join("values").join("colors.xml"),
include_bytes!("../../assets/android/gen/app/src/main/res/values/colors.xml"),
)?;
write(
res.join("values").join("styles.xml"),
include_bytes!("../../assets/android/gen/app/src/main/res/values/styles.xml"),
)?;
create_dir_all(res.join("drawable"))?;
write(
res.join("drawable").join("ic_launcher_background.xml"),
include_bytes!(
"../../assets/android/gen/app/src/main/res/drawable/ic_launcher_background.xml"
),
)?;
create_dir_all(res.join("drawable-v24"))?;
write(
res.join("drawable-v24").join("ic_launcher_foreground.xml"),
include_bytes!(
"../../assets/android/gen/app/src/main/res/drawable-v24/ic_launcher_foreground.xml"
),
)?;
create_dir_all(res.join("mipmap-anydpi-v26"))?;
write(
res.join("mipmap-anydpi-v26").join("ic_launcher.xml"),
include_bytes!(
"../../assets/android/gen/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml"
),
)?;
create_dir_all(res.join("mipmap-hdpi"))?;
write(
res.join("mipmap-hdpi").join("ic_launcher.webp"),
include_bytes!(
"../../assets/android/gen/app/src/main/res/mipmap-hdpi/ic_launcher.webp"
),
)?;
create_dir_all(res.join("mipmap-mdpi"))?;
write(
res.join("mipmap-mdpi").join("ic_launcher.webp"),
include_bytes!(
"../../assets/android/gen/app/src/main/res/mipmap-mdpi/ic_launcher.webp"
),
)?;
create_dir_all(res.join("mipmap-xhdpi"))?;
write(
res.join("mipmap-xhdpi").join("ic_launcher.webp"),
include_bytes!(
"../../assets/android/gen/app/src/main/res/mipmap-xhdpi/ic_launcher.webp"
),
)?;
create_dir_all(res.join("mipmap-xxhdpi"))?;
write(
res.join("mipmap-xxhdpi").join("ic_launcher.webp"),
include_bytes!(
"../../assets/android/gen/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp"
),
)?;
create_dir_all(res.join("mipmap-xxxhdpi"))?;
write(
res.join("mipmap-xxxhdpi").join("ic_launcher.webp"),
include_bytes!(
"../../assets/android/gen/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp"
),
)?;
Ok(())
}
}
+7
View File
@@ -0,0 +1,7 @@
#[derive(serde::Serialize)]
pub struct InfoPlistData {
pub display_name: String,
pub bundle_name: String,
pub bundle_identifier: String,
pub executable_name: String,
}
+148
View File
@@ -0,0 +1,148 @@
use std::process::Stdio;
use crate::{BuildRequest, Platform, Result, RustupShow};
use anyhow::Context;
use tokio::process::Command;
impl BuildRequest {
/// Install any 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 rustup = match RustupShow::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(rustup).await?,
Platform::Ios => self.verify_ios_tooling(rustup).await?,
Platform::Android => self.verify_android_tooling(rustup).await?,
Platform::Linux => self.verify_linux_tooling(rustup).await?,
Platform::MacOS => {}
Platform::Windows => {}
Platform::Server => {}
Platform::Liveview => {}
}
Ok(())
}
pub(crate) async fn verify_web_tooling(&self, rustup: RustupShow) -> Result<()> {
if !rustup.has_wasm32_unknown_unknown() {
tracing::info!(
"Web platform requires wasm32-unknown-unknown to be installed. Installing..."
);
let _ = Command::new("rustup")
.args(["target", "add", "wasm32-unknown-unknown"])
.output()
.await?;
}
match self.krate.wasm_bindgen_version() {
Some(version) if version == wasm_bindgen_shared::SCHEMA_VERSION => {
tracing::debug!("wasm-bindgen version {version} is compatible with dioxus-cli ✅");
},
Some(version) => {
tracing::warn!(
"wasm-bindgen version {version} is not compatible with the cli crate. Attempting to upgrade the target wasm-bindgen crate manually..."
);
let output = Command::new("cargo")
.args([
"update",
"-p",
"wasm-bindgen",
"--precise",
&wasm_bindgen_shared::version(),
])
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.output()
.await;
match output {
Ok(output) if output.status.success() => tracing::info!("✅ wasm-bindgen updated successfully"),
Ok(output) => tracing::error!("Failed to update wasm-bindgen: {:?}", output),
Err(err) => tracing::error!("Failed to update wasm-bindgen: {err}"),
}
}
None => tracing::debug!("User is attempting a web build without wasm-bindgen detected. This is probably a bug in the dioxus-cli."),
}
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, _rustup: RustupShow) -> 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, _rustup: RustupShow) -> Result<()> {
Ok(())
}
/// 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, _rustup: crate::RustupShow) -> Result<()> {
Ok(())
}
}
+196
View File
@@ -0,0 +1,196 @@
use crate::error::Result;
use crate::BuildRequest;
use std::fmt::Write;
use std::path::{Path, PathBuf};
const DEFAULT_HTML: &str = include_str!("../../assets/web/index.html");
const TOAST_HTML: &str = include_str!("../../assets/web/toast.html");
impl BuildRequest {
pub(crate) fn prepare_html(&self) -> Result<String> {
let mut html = {
let crate_root: &Path = &self.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.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.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.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(),
)?;
}
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
// if let Some(assets) = assets {
// head_resources.push_str(&assets.head());
// }
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.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}/wasm/{app_name}.js").then(
({ default: init }) => {
init("/{base_path}/wasm/{app_name}_bg.wasm").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}", ""),
};
// And try to insert preload links for the wasm and js files
*html = html.replace(
"</head",
r#"<link rel="preload" href="/{base_path}/wasm/{app_name}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="preload" href="/{base_path}/wasm/{app_name}.js" as="script">
</head"#
);
}
/// Replace any special placeholders in the HTML with resolved values
fn replace_template_placeholders(&self, html: &mut String) {
let base_path = self.krate.config.web.app.base_path();
*html = html.replace("{base_path}", base_path);
let app_name = &self.krate.executable_name();
*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.krate.legacy_asset_dir().join(path);
if let Ok(absolute_path) = asset_dir_path.canonicalize() {
let absolute_crate_root = self.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::debug!(
"{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);
}
}