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

@@ -1,6 +1,6 @@
use crate::{
AppBundle, BuildArgs, BuildRequest, BuildStage, BuildUpdate, DioxusCrate, Platform, ProgressRx,
ProgressTx, Result,
AppBundle, BuildArgs, BuildRequest, BuildStage, BuildUpdate, DioxusCrate, ProgressRx,
ProgressTx, Result, StructuredOutput,
};
use std::time::{Duration, Instant};
@@ -40,6 +40,7 @@ impl Builder {
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(),
@@ -49,14 +50,7 @@ impl Builder {
// We wont bother verifying on subsequent builds
request.verify_tooling().await?;
let res = request.build_all().await;
// The first launch gets some extra logging :)
if res.is_ok() {
tracing::info!("Build completed successfully, launching app! 💫")
}
res
request.build_all().await
}),
tx,
rx,
@@ -91,79 +85,76 @@ impl Builder {
},
};
tracing::trace!("Build update: {update:?}");
// 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,
platform,
} => {
if *platform == Platform::Server {
self.expected_crates_server = *crate_count;
} else {
self.expected_crates = *crate_count;
match stage {
BuildStage::Initializing => {
self.compiled_crates = 0;
self.compiled_crates_server = 0;
self.bundling_progress = 0.0;
}
}
BuildStage::InstallingTooling {} => {}
BuildStage::Compiling {
current,
total,
platform,
..
} => {
if *platform == Platform::Server {
self.compiled_crates_server = *current;
self.expected_crates_server = *total;
} else {
self.compiled_crates = *current;
self.expected_crates = *total;
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());
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 {} => {
self.bundling_progress = 0.5;
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 {} => {}
_ => {}
}
}
}
@@ -224,10 +215,43 @@ impl Builder {
pub(crate) async fn finish(&mut self) -> Result<AppBundle> {
loop {
match self.wait().await {
BuildUpdate::BuildReady { bundle } => return Ok(bundle),
BuildUpdate::BuildFailed { err } => return Err(err),
BuildUpdate::Progress { .. } => {}
BuildUpdate::CompilerMessage { .. } => {}
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);
}
}
}
}
@@ -275,7 +299,7 @@ impl Builder {
self.bundling_progress
}
fn is_finished(&self) -> bool {
pub(crate) fn is_finished(&self) -> bool {
match self.stage {
BuildStage::Success => true,
BuildStage::Failed => true,
@@ -3,13 +3,16 @@ use crate::{assets::AssetManifest, TraceSrc};
use crate::{BuildRequest, Platform};
use anyhow::Context;
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use std::sync::atomic::AtomicUsize;
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.
@@ -93,13 +96,14 @@ pub(crate) struct AppBundle {
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/static-gen), we still use the `web` folder, but it only contains the
/// If there's no server (SPA), we still use the `web` folder, but it only contains the
/// public folder.
/// ```
/// web/
@@ -255,52 +259,40 @@ impl AppBundle {
/// root().join(bundled)
/// ```
pub(crate) async fn new(
request: BuildRequest,
build: BuildRequest,
app: BuildArtifacts,
server: Option<BuildArtifacts>,
) -> Result<Self> {
let bundle = Self {
app,
server,
build: request,
};
let bundle = Self { app, server, build };
tracing::debug!("Assembling app bundle");
bundle.build.status_start_bundle();
bundle.prepare_build_dir()?;
bundle.write_main_executable().await?;
/*
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?;
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)
}
/// 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<()> {
_ = std::fs::remove_dir_all(self.app_dir());
create_dir_all(self.app_dir())?;
create_dir_all(self.exe_dir())?;
create_dir_all(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.build.platform() {}
Ok(())
}
/// 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
@@ -333,18 +325,17 @@ impl AppBundle {
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.exe_dir())
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.build.status_wasm_opt_start();
self.run_wasm_opt(&self.exe_dir())?;
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.app_dir().join("index.html"),
self.build.root_dir().join("index.html"),
self.build.prepare_html()?,
)?;
}
@@ -385,7 +376,7 @@ impl AppBundle {
return Ok(());
}
let asset_dir = self.asset_dir();
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
@@ -418,10 +409,9 @@ impl AppBundle {
// 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)| {
self.build.status_copying_asset(
assets_finished.fetch_add(0, std::sync::atomic::Ordering::SeqCst),
asset_count,
from.clone(),
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
@@ -431,7 +421,7 @@ impl AppBundle {
tracing::error!("Failed to copy asset {from:?}: {err}");
}
self.build.status_copying_asset(
self.build.status_copied_asset(
assets_finished.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1,
asset_count,
from.clone(),
@@ -443,66 +433,11 @@ impl AppBundle {
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.build.platform() {
Platform::MacOS => self.app_dir().join("Contents").join("MacOS"),
Platform::Android => self.app_dir().join("jniLibs").join("arm64-v8a"),
Platform::Web => self.app_dir().join("wasm"),
// these are all the same, I think?
Platform::Windows
| Platform::Linux
| Platform::Ios
| Platform::Server
| Platform::Liveview => self.app_dir(),
}
}
/// 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 {
// todo(jon): this could just be named `App` or the name of the app like `Raycast` in `Raycast.app`
match self.build.build.platform() {
Platform::MacOS => self.exe_dir().join("DioxusApp"),
Platform::Ios => self.exe_dir().join("DioxusApp"),
Platform::Server => self.exe_dir().join("server"),
Platform::Liveview => self.exe_dir().join("server"),
Platform::Windows => self.exe_dir().join("app.exe"),
Platform::Linux => self.exe_dir().join("AppRun"), // from the appimage spec, the root exe needs to be named `AppRun`
Platform::Android => self.exe_dir().join("libdioxusapp.so"), // from the apk spec, the root exe will actually be a shared library
Platform::Web => unimplemented!("there's no main exe on web"), // this will be wrong, I think, but not important?
}
}
pub fn asset_dir(&self) -> PathBuf {
match self.build.build.platform() {
// macos why are you weird
Platform::MacOS => self
.app_dir()
.join("Contents")
.join("Resources")
.join("assets"),
// everyone else is soooo normal, just app/assets :)
Platform::Web
| Platform::Ios
| Platform::Windows
| Platform::Linux
| Platform::Android
| Platform::Server
| Platform::Liveview => self.app_dir().join("assets"),
}
self.build.exe_dir().join(self.build.platform_exe_name())
}
/// We always put the server in the `web` folder!
@@ -531,15 +466,15 @@ impl AppBundle {
// write the Info.plist file
match self.build.build.platform() {
Platform::MacOS => {
let src = include_str!("../../assets/macos/mac.plist");
let dest = self.app_dir().join("Contents").join("Info.plist");
std::fs::write(dest, src)?;
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 src = include_str!("../../assets/ios/ios.plist");
let dest = self.app_dir().join("Info.plist");
std::fs::write(dest, src)?;
let dest = self.build.root_dir().join("Info.plist");
let plist = self.ios_plist_contents()?;
std::fs::write(dest, plist)?;
}
// AndroidManifest.xml
@@ -575,7 +510,7 @@ impl AppBundle {
.krate
.should_pre_compress_web_assets(self.build.build.release);
let bindgen_dir = self.exe_dir();
let bindgen_dir = self.build.exe_dir();
tokio::task::spawn_blocking(move || {
crate::fastfs::pre_compress_folder(&bindgen_dir, pre_compress)
})
@@ -618,37 +553,6 @@ impl AppBundle {
None
}
/// returns the path to .app/.apk/.appimage folder
///
/// 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
pub(crate) fn app_dir(&self) -> PathBuf {
let platform_dir = self
.build
.krate
.build_dir(self.build.build.platform(), self.build.build.release);
match self.build.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("DioxusApp.app"),
Platform::Ios => platform_dir.join("DioxusApp.app"),
// in theory, these all could end up in the build dir
Platform::Linux => platform_dir.join("app"), // .appimage (after bundling)
Platform::Windows => platform_dir.join("app"), // .exe (after bundling)
Platform::Android => platform_dir.join("app"), // .apk (after bundling)
Platform::Liveview => platform_dir.join("app"), // .exe (after bundling)
}
}
pub(crate) async fn run_wasm_bindgen(
&self,
input_path: &Path,
@@ -691,7 +595,6 @@ impl AppBundle {
if !self.build.build.release {
return Ok(());
};
self.build.status_optimizing_wasm();
#[cfg(feature = "optimizations")]
@@ -799,4 +702,76 @@ impl AppBundle {
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")
}
}
@@ -5,14 +5,15 @@
//! Uses a request -> response architecture that allows you to monitor the progress with an optional message
//! receiver.
mod build;
mod builder;
mod bundle;
mod progress;
mod runner;
mod request;
mod templates;
mod verify;
mod web;
pub(crate) use build::*;
pub(crate) use builder::*;
pub(crate) use bundle::*;
pub(crate) use progress::*;
pub(crate) use runner::*;
pub(crate) use request::*;
@@ -1,5 +1,5 @@
//! Report progress about the build to the user. We use channels to report progress back to the CLI.
use crate::{AppBundle, BuildRequest, Platform, TraceSrc};
use crate::{AppBundle, BuildRequest, BuildStage, Platform, TraceSrc};
use cargo_metadata::CompilerMessage;
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
use std::path::PathBuf;
@@ -16,46 +16,12 @@ pub(crate) enum BuildUpdate {
BuildFailed { err: crate::Error },
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum BuildStage {
Initializing,
Starting {
platform: Platform,
crate_count: usize,
},
InstallingTooling {},
Compiling {
platform: Platform,
current: usize,
total: usize,
krate: String,
},
Bundling {},
RunningBindgen {},
OptimizingWasm {},
CopyingAssets {
current: usize,
total: usize,
path: PathBuf,
},
Success,
Failed,
Aborted,
Restarting,
}
impl BuildRequest {
pub(crate) fn status_wasm_bindgen_start(&self) {
_ = self.progress.unbounded_send(BuildUpdate::Progress {
stage: BuildStage::RunningBindgen {},
});
}
pub(crate) fn status_wasm_opt_start(&self) {
_ = self.progress.unbounded_send(BuildUpdate::Progress {
stage: BuildStage::RunningBindgen {},
});
}
pub(crate) fn status_start_bundle(&self) {
_ = self.progress.unbounded_send(BuildUpdate::Progress {
@@ -73,19 +39,13 @@ impl BuildRequest {
tracing::trace!(dx_src = ?TraceSrc::Cargo, "{line}");
}
pub(crate) fn status_build_progress(
&self,
count: usize,
total: usize,
name: String,
platform: Platform,
) {
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,
platform,
is_server: self.is_server(),
},
});
}
@@ -93,14 +53,13 @@ impl BuildRequest {
pub(crate) fn status_starting_build(&self, crate_count: usize) {
_ = self.progress.unbounded_send(BuildUpdate::Progress {
stage: BuildStage::Starting {
platform: self.build.platform(),
is_server: self.build.platform() == Platform::Server,
crate_count,
},
});
}
pub(crate) fn status_copying_asset(&self, current: usize, total: usize, path: PathBuf) {
tracing::trace!("Status copying asset {current}/{total} from {path:?}");
pub(crate) fn status_copied_asset(&self, current: usize, total: usize, path: PathBuf) {
_ = self.progress.unbounded_send(BuildUpdate::Progress {
stage: BuildStage::CopyingAssets {
current,
@@ -121,4 +80,8 @@ impl BuildRequest {
stage: BuildStage::InstallingTooling {},
});
}
pub(crate) fn is_server(&self) -> bool {
self.build.platform() == Platform::Server
}
}
@@ -2,14 +2,11 @@ use super::{progress::ProgressTx, BuildArtifacts};
use crate::dioxus_crate::DioxusCrate;
use crate::Result;
use crate::{assets::AssetManifest, TraceSrc};
use crate::{build::BuildArgs, link::LinkAction};
use crate::{link::LinkAction, BuildArgs};
use crate::{AppBundle, Platform};
use anyhow::Context;
use serde::Deserialize;
use std::{
path::{Path, PathBuf},
process::Stdio,
};
use std::{path::PathBuf, process::Stdio, time::Instant};
use tokio::{io::AsyncBufReadExt, process::Command};
#[derive(Clone, Debug)]
@@ -44,17 +41,32 @@ impl BuildRequest {
pub(crate) async fn build_all(self) -> Result<AppBundle> {
tracing::debug!("Running build command...");
let (app, server) =
futures_util::future::try_join(self.build_app(), self.build_server()).await?;
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(&exe).await?;
Ok(BuildArtifacts { exe, assets })
let assets = self.collect_assets().await?;
Ok(BuildArtifacts {
exe,
assets,
time_taken: start.elapsed(),
})
}
pub(crate) async fn build_server(&self) -> Result<Option<BuildArtifacts>> {
@@ -87,8 +99,9 @@ impl BuildRequest {
.current_dir(self.krate.crate_dir())
.arg("--message-format")
.arg("json-diagnostic-rendered-ansi")
.args(self.build_arguments());
// .env("RUSTFLAGS", self.rust_flags());
.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);
@@ -153,7 +166,6 @@ impl BuildRequest {
units_compiled,
crate_count,
artifact.target.name,
self.build.platform(),
),
}
}
@@ -188,19 +200,14 @@ impl BuildRequest {
/// 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, exe: &Path) -> Result<AssetManifest> {
pub(crate) async fn collect_assets(&self) -> Result<AssetManifest> {
tracing::debug!("Collecting assets ...");
// If assets are skipped, we don't need to collect them
if self.build.skip_assets {
return Ok(AssetManifest::default());
}
let mut manifest = AssetManifest::default();
_ = manifest.add_from_object_path(exe.to_path_buf());
Ok(manifest)
self.deep_linker_asset_extract().await
}
/// Create a list of arguments for cargo builds
@@ -254,11 +261,8 @@ impl BuildRequest {
}
}
if self.build.verbose {
cargo_args.push("--verbose".to_string());
} else {
cargo_args.push("--quiet".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());
@@ -293,7 +297,7 @@ impl BuildRequest {
}
#[allow(dead_code)]
pub(crate) fn rust_flags(&self) -> String {
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 {
@@ -355,6 +359,7 @@ impl BuildRequest {
.arg("-Z")
.arg("unstable-options")
.args(self.build_arguments())
.envs(self.env_vars())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
@@ -394,7 +399,6 @@ impl BuildRequest {
///
/// 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".
#[allow(unused)]
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
@@ -406,9 +410,9 @@ impl BuildRequest {
//
// This will force `dx` to look through the incremental cache and find the assets from the previous build
Command::new("cargo")
// .env("RUSTFLAGS", self.rust_flags())
.arg("rustc")
.args(self.build_arguments())
.envs(self.env_vars())
.arg("--offline") /* don't use the network, should already be resolved */
.arg("--")
.arg(format!(
@@ -422,7 +426,7 @@ impl BuildRequest {
.env(
LinkAction::ENV_VAR_NAME,
LinkAction::BuildAssetManifest {
destination: tmp_file.path().to_path_buf(),
destination: tmp_file.path().to_path_buf().clone(),
}
.to_json(),
)
@@ -432,6 +436,351 @@ impl BuildRequest {
.await?;
// The linker wrote the manifest to the temp file, let's load it!
Ok(AssetManifest::load_from_file(tmp_file.path())?)
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,
}
@@ -94,21 +94,21 @@ impl BuildRequest {
/// 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;
// _ = 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;
// _ = 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
File renamed without changes.
+1 -6
View File
@@ -3,12 +3,6 @@ use crate::{
NSISInstallerMode, NsisSettings, PackageType, WebviewInstallMode, WindowsSettings, WixSettings,
};
pub(crate) fn make_tauri_bundler_settings(
bundle_config: BundleConfig,
) -> tauri_bundler::BundleSettings {
bundle_config.into()
}
impl From<NsisSettings> for tauri_bundler::NsisSettings {
fn from(val: NsisSettings) -> Self {
tauri_bundler::NsisSettings {
@@ -162,6 +156,7 @@ impl From<PackageType> for tauri_bundler::PackageType {
PackageType::AppImage => Self::AppImage,
PackageType::Dmg => Self::Dmg,
PackageType::Updater => Self::Updater,
PackageType::Nsis => Self::Nsis,
}
}
}
+14 -23
View File
@@ -1,8 +1,9 @@
use super::*;
use crate::DioxusCrate;
use anyhow::Context;
use dioxus_autofmt::{IndentOptions, IndentType};
use rayon::prelude::*;
use std::{borrow::Cow, fs, path::Path, process::exit};
use std::{borrow::Cow, fs, path::Path};
// For reference, the rustfmt main.rs file
// https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs
@@ -37,7 +38,7 @@ pub(crate) struct Autoformat {
}
impl Autoformat {
pub(crate) fn autoformat(self) -> Result<()> {
pub(crate) fn autoformat(self) -> Result<StructuredOutput> {
let Autoformat {
check,
raw,
@@ -56,9 +57,7 @@ impl Autoformat {
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
println!("{}", inner);
} else {
// exit process with error
eprintln!("error formatting codeblock");
exit(1);
return Err("error formatting codeblock".into());
}
} else {
// Default to formatting the project.
@@ -68,13 +67,9 @@ impl Autoformat {
package: Some(package),
..Default::default()
};
let dx_crate = match DioxusCrate::new(&target_args) {
Ok(x) => x,
Err(error) => {
eprintln!("failed to parse crate graph: {error}");
exit(1);
}
};
let dx_crate =
DioxusCrate::new(&target_args).context("failed to parse crate graph")?;
Cow::Owned(dx_crate.crate_dir())
} else {
Cow::Borrowed(Path::new("."))
@@ -83,12 +78,11 @@ impl Autoformat {
if let Err(e) =
autoformat_project(check, split_line_attributes, format_rust_code, crate_dir)
{
eprintln!("error formatting project: {}", e);
exit(1);
return Err(format!("error formatting project: {}", e).into());
}
}
Ok(())
Ok(StructuredOutput::Success)
}
}
@@ -106,8 +100,7 @@ fn refactor_file(
fs::read_to_string(&file)
};
let Ok(mut s) = file_content else {
eprintln!("failed to open file: {}", file_content.unwrap_err());
exit(1);
return Err(format!("failed to open file: {}", file_content.unwrap_err()).into());
};
if format_rust_code {
@@ -117,8 +110,7 @@ fn refactor_file(
let Ok(Ok(edits)) =
syn::parse_file(&s).map(|file| dioxus_autofmt::try_fmt_file(&s, &file, indent))
else {
eprintln!("failed to format file: {}", s);
exit(1);
return Err(format!("failed to format file: {}", s).into());
};
let out = dioxus_autofmt::apply_formats(&s, edits);
@@ -126,7 +118,7 @@ fn refactor_file(
if file == "-" {
print!("{}", out);
} else if let Err(e) = fs::write(&file, out) {
eprintln!("failed to write formatted content to file: {e}",);
tracing::error!("failed to write formatted content to file: {e}",);
} else {
println!("formatted {}", file);
}
@@ -212,7 +204,7 @@ fn autoformat_project(
match res {
Ok(cnt) => Some(cnt),
Err(err) => {
eprintln!("error formatting file : {}\n{:#?}", path.display(), err);
tracing::error!("error formatting file : {}\n{:#?}", path.display(), err);
None
}
}
@@ -222,8 +214,7 @@ fn autoformat_project(
let files_formatted: usize = counts.into_iter().flatten().sum();
if files_formatted > 0 && check {
eprintln!("{} files needed formatting", files_formatted);
exit(1);
return Err(format!("{} files needed formatting", files_formatted).into());
}
Ok(())
+10 -30
View File
@@ -1,11 +1,10 @@
use super::*;
use crate::{AppBundle, Builder, DioxusCrate, Platform, PROFILE_SERVER};
use crate::{Builder, DioxusCrate, Platform, PROFILE_SERVER};
/// Build the Rust Dioxus app and all of its assets.
///
/// Produces a final output bundle designed to be run on the target platform.
#[derive(Clone, Debug, Default, Deserialize, Parser)]
#[clap(name = "build")]
pub(crate) struct BuildArgs {
/// Build in release mode [default: false]
#[clap(long, short)]
@@ -17,21 +16,6 @@ pub(crate) struct BuildArgs {
#[serde(default)]
pub(crate) force_sequential: bool,
/// Use verbose output [default: false]
#[clap(long)]
#[serde(default)]
pub(crate) verbose: bool,
/// Use trace output [default: false]
#[clap(long)]
#[serde(default)]
pub(crate) trace: bool,
/// Pass -Awarnings to the cargo build
#[clap(long)]
#[serde(default)]
pub(crate) silent: bool,
/// Build the app with custom a profile
#[clap(long)]
pub(crate) profile: Option<String>,
@@ -73,12 +57,9 @@ pub(crate) struct BuildArgs {
}
impl BuildArgs {
pub async fn build_it(&mut self) -> Result<()> {
self.build().await?;
Ok(())
}
pub async fn run_cmd(mut self) -> Result<StructuredOutput> {
tracing::info!("Building project...");
pub(crate) async fn build(&mut self) -> Result<AppBundle> {
let krate =
DioxusCrate::new(&self.target_args).context("Failed to load Dioxus workspace")?;
@@ -86,12 +67,11 @@ impl BuildArgs {
let bundle = Builder::start(&krate, self.clone())?.finish().await?;
println!(
"Successfully built! 💫\nBundle at {}",
bundle.app_dir().display()
);
tracing::info!(path = ?bundle.build.root_dir(), "Build completed successfully! 🚀");
Ok(bundle)
Ok(StructuredOutput::BuildFinished {
path: bundle.build.root_dir(),
})
}
/// Update the arguments of the CLI by inspecting the DioxusCrate itself and learning about how
@@ -127,13 +107,13 @@ impl BuildArgs {
// Add any features required to turn on the client
self.target_args
.client_features
.extend(krate.feature_for_platform(platform));
.push(krate.feature_for_platform(platform));
// Add any features required to turn on the server
// This won't take effect in the server is not built, so it's fine to just set it here even if it's not used
self.target_args
.server_features
.extend(krate.feature_for_platform(Platform::Server));
.push(krate.feature_for_platform(Platform::Server));
// Make sure we set the fullstack platform so we actually build the fullstack variant
// Users need to enable "fullstack" in their default feature set.
@@ -150,7 +130,7 @@ impl BuildArgs {
// Set the profile of the build if it's not already set
// We do this for android/wasm since they require
if self.profile.is_none() {
if self.profile.is_none() && !self.release {
match self.platform {
Some(Platform::Android) => {
self.profile = Some(crate::dioxus_crate::PROFILE_ANDROID.to_string());
+164 -77
View File
@@ -1,20 +1,36 @@
use crate::bundle_utils::make_tauri_bundler_settings;
use crate::DioxusCrate;
use crate::{build::BuildArgs, PackageType};
use crate::{AppBundle, BuildArgs, Builder, DioxusCrate, Platform};
use anyhow::Context;
use std::env::current_dir;
use std::str::FromStr;
use tauri_bundler::{PackageSettings, SettingsBuilder};
use std::collections::HashMap;
use tauri_bundler::{BundleBinary, BundleSettings, PackageSettings, SettingsBuilder};
use super::*;
/// Bundle the Rust desktop app and all of its assets
#[derive(Clone, Debug, Parser)]
#[clap(name = "bundle")]
pub struct Bundle {
/// The package types to bundle
///
/// Any of:
/// - macos: The macOS application bundle (.app).
/// - ios: The iOS app bundle.
/// - msi: The Windows bundle (.msi).
/// - nsis: The NSIS bundle (.exe).
/// - deb: The Linux Debian package bundle (.deb).
/// - rpm: The Linux RPM bundle (.rpm).
/// - appimage: The Linux AppImage bundle (.AppImage).
/// - dmg: The macOS DMG bundle (.dmg).
/// - updater: The Updater bundle.
#[clap(long)]
pub packages: Option<Vec<PackageType>>,
pub package_types: Option<Vec<crate::PackageType>>,
/// The directory in which the final bundle will be placed.
///
/// Relative paths will be placed relative to the current working directory.
///
/// We will flatten the artifacts into this directory - there will be no differentiation between
/// artifacts produced by different platforms.
#[clap(long)]
pub outdir: Option<PathBuf>,
/// The arguments for the dioxus build
#[clap(flatten)]
@@ -22,31 +38,124 @@ pub struct Bundle {
}
impl Bundle {
pub(crate) async fn bundle(mut self) -> anyhow::Result<()> {
pub(crate) async fn bundle(mut self) -> Result<StructuredOutput> {
tracing::info!("Bundling project...");
let krate = DioxusCrate::new(&self.build_arguments.target_args)
.context("Failed to load Dioxus workspace")?;
// We always use `release` mode for bundling
self.build_arguments.release = true;
self.build_arguments.resolve(&krate)?;
// Build the app
let bundle = self.build_arguments.build().await?;
tracing::info!("Building app...");
let bundle = Builder::start(&krate, self.build_arguments.clone())?
.finish()
.await?;
tracing::info!("Copying app to output directory...");
// If we're building for iOS, we need to bundle the iOS bundle
if self.build_arguments.platform() == Platform::Ios && self.package_types.is_none() {
self.package_types = Some(vec![crate::PackageType::IosBundle]);
}
let mut cmd_result = StructuredOutput::Success;
match self.build_arguments.platform() {
// By default, mac/win/linux work with tauri bundle
Platform::MacOS | Platform::Linux | Platform::Windows => {
let bundles = self.bundle_desktop(krate, bundle)?;
tracing::info!("Bundled app successfully!");
tracing::info!("App produced {} outputs:", bundles.len());
tracing::debug!("Bundling produced bundles: {:#?}", bundles);
// Copy the bundles to the output directory and log their locations
let mut bundle_paths = vec![];
for bundle in bundles {
for src in bundle.bundle_paths {
let src = if let Some(outdir) = &self.outdir {
let dest = outdir.join(src.file_name().unwrap());
crate::fastfs::copy_asset(&src, &dest)?;
dest
} else {
src.clone()
};
tracing::info!(
"{} - [{}]",
bundle.package_type.short_name(),
src.display()
);
bundle_paths.push(src);
}
}
cmd_result = StructuredOutput::BundleOutput {
bundles: bundle_paths,
};
}
Platform::Web => {
tracing::info!("App available at: {}", bundle.build.root_dir().display());
}
Platform::Ios => {
tracing::warn!("Signed iOS bundles are not yet supported");
tracing::info!(
"The bundle is available at: {}",
bundle.build.root_dir().display()
);
}
Platform::Server => {
tracing::info!("Server available at: {}", bundle.build.root_dir().display())
}
Platform::Liveview => tracing::info!(
"Liveview server available at: {}",
bundle.build.root_dir().display()
),
Platform::Android => {
return Err(Error::UnsupportedFeature(
"Android bundles are not yet supported".into(),
));
}
};
Ok(cmd_result)
}
fn bundle_desktop(
&self,
krate: DioxusCrate,
bundle: AppBundle,
) -> Result<Vec<tauri_bundler::Bundle>, Error> {
_ = std::fs::remove_dir_all(krate.bundle_dir(self.build_arguments.platform()));
// copy the binary to the out dir
let package = krate.package();
let mut name: PathBuf = krate.executable_name().into();
if cfg!(windows) {
name.set_extension("exe");
}
std::fs::create_dir_all(krate.bundle_dir(self.build_arguments.platform()))?;
std::fs::copy(
&bundle.app.exe,
krate
.bundle_dir(self.build_arguments.platform())
.join(krate.executable_name()),
)?;
// bundle the app
let binaries = vec![
tauri_bundler::BundleBinary::new(name.display().to_string(), true)
.set_src_path(Some(krate.workspace_dir().display().to_string())),
// We use the name of the exe but it has to be in the same directory
BundleBinary::new(name.display().to_string(), true)
.set_src_path(Some(bundle.app.exe.display().to_string())),
];
let bundle_config = krate.config.bundle.clone();
let mut bundle_settings = make_tauri_bundler_settings(bundle_config);
let mut bundle_settings: BundleSettings = krate.config.bundle.clone().into();
if cfg!(windows) {
let windows_icon_override = krate.config.bundle.windows.as_ref().map(|w| &w.icon_path);
@@ -62,38 +171,28 @@ impl Bundle {
}
}
// Don't copy the executable or the old bundle directory
let ignored_files = [krate
.bundle_dir(self.build_arguments.platform())
.join("bundle")];
if bundle_settings.resources_map.is_none() {
bundle_settings.resources_map = Some(HashMap::new());
}
for entry in std::fs::read_dir(bundle.asset_dir())?.flatten() {
let path = entry.path().canonicalize()?;
if ignored_files.iter().any(|f| path.starts_with(f)) {
continue;
}
for entry in std::fs::read_dir(bundle.build.asset_dir())?.flatten() {
let old = entry.path().canonicalize()?;
let new = PathBuf::from("assets").join(old.file_name().unwrap());
tracing::debug!("Bundled asset: {old:?} -> {new:?}");
// Tauri bundle will add a __root__ prefix if the input path is absolute even though the output path is relative?
// We strip the prefix here to make sure the input path is relative so that the bundler puts the output path in the right place
let path = path
.strip_prefix(&current_dir()?)
.unwrap()
.display()
.to_string();
if let Some(resources) = &mut bundle_settings.resources_map {
resources.insert(path, "".to_string());
} else {
bundle_settings.resources_map = Some([(path, "".to_string())].into());
}
bundle_settings
.resources_map
.as_mut()
.expect("to be set")
.insert(old.display().to_string(), new.display().to_string());
}
// Drain any resources set in the config into the resources map. Tauri bundle doesn't let you set both resources and resources_map https://github.com/DioxusLabs/dioxus/issues/2941
for resource_path in bundle_settings.resources.take().into_iter().flatten() {
if let Some(resources) = &mut bundle_settings.resources_map {
resources.insert(resource_path, "".to_string());
} else {
bundle_settings.resources_map = Some([(resource_path, "".to_string())].into());
}
bundle_settings
.resources_map
.as_mut()
.expect("to be set")
.insert(resource_path, "".to_string());
}
let mut settings = SettingsBuilder::new()
@@ -106,9 +205,11 @@ impl Bundle {
authors: Some(package.authors.clone()),
default_run: Some(krate.executable_name().to_string()),
})
.log_level(log::Level::Debug)
.binaries(binaries)
.bundle_settings(bundle_settings);
if let Some(packages) = &self.packages {
if let Some(packages) = &self.package_types {
settings = settings.package_types(packages.iter().map(|p| (*p).into()).collect());
}
@@ -116,37 +217,23 @@ impl Bundle {
settings = settings.target(target.to_string());
}
let settings = settings.build();
// on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567)
#[cfg(target_os = "macos")]
std::env::set_var("CI", "true");
tauri_bundler::bundle::bundle_project(&settings.unwrap()).unwrap_or_else(|err|{
#[cfg(target_os = "macos")]
panic!("Failed to bundle project: {:#?}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err);
#[cfg(not(target_os = "macos"))]
panic!("Failed to bundle project: {:#?}", err);
});
Ok(())
}
}
impl FromStr for PackageType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"macos" => Ok(PackageType::MacOsBundle),
"ios" => Ok(PackageType::IosBundle),
"msi" => Ok(PackageType::WindowsMsi),
"deb" => Ok(PackageType::Deb),
"rpm" => Ok(PackageType::Rpm),
"appimage" => Ok(PackageType::AppImage),
"dmg" => Ok(PackageType::Dmg),
"updater" => Ok(PackageType::Updater),
_ => Err(format!("{} is not a valid package type", s)),
if self.build_arguments.platform() == Platform::Ios {
settings = settings.target("aarch64-apple-ios".to_string());
}
let settings = settings.build()?;
tracing::debug!("Bundling project with settings: {:#?}", settings);
if cfg!(target_os = "macos") {
std::env::set_var("CI", "true");
}
let bundles = tauri_bundler::bundle::bundle_project(&settings).inspect_err(|err| {
tracing::error!("Failed to bundle project: {:#?}", err);
if cfg!(target_os = "macos") {
tracing::error!("Make sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)");
}
})?;
Ok(bundles)
}
}
+23 -25
View File
@@ -1,10 +1,13 @@
//! Run linting against the user's codebase.
//!
//! For reference, the rustfmt main.rs file
//! https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs
use super::*;
use crate::DioxusCrate;
use anyhow::Context;
use futures_util::{stream::FuturesUnordered, StreamExt};
use std::{path::Path, process::exit};
// For reference, the rustfmt main.rs file
// https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs
use std::path::Path;
/// Check the Rust files in the project for issues.
#[derive(Clone, Debug, Parser)]
@@ -20,25 +23,23 @@ pub(crate) struct Check {
impl Check {
// Todo: check the entire crate
pub(crate) async fn check(self) -> Result<()> {
pub(crate) async fn check(self) -> Result<StructuredOutput> {
match self.file {
// Default to checking the project
None => {
let dioxus_crate = DioxusCrate::new(&self.target_args)?;
if let Err(e) = check_project_and_report(dioxus_crate).await {
eprintln!("error checking project: {}", e);
exit(1);
}
check_project_and_report(dioxus_crate)
.await
.context("error checking project")?;
}
Some(file) => {
if let Err(e) = check_file_and_report(file).await {
eprintln!("failed to check file: {}", e);
exit(1);
}
check_file_and_report(file)
.await
.context("error checking file")?;
}
}
Ok(())
Ok(StructuredOutput::Success)
}
}
@@ -72,7 +73,7 @@ async fn check_files_and_report(files_to_check: Vec<PathBuf>) -> Result<()> {
.await;
if res.is_err() {
eprintln!("error checking file: {}", path.display());
tracing::error!("error checking file: {}", path.display());
}
res
@@ -92,19 +93,17 @@ async fn check_files_and_report(files_to_check: Vec<PathBuf>) -> Result<()> {
for report in issue_reports.into_iter() {
if !report.issues.is_empty() {
println!("{}", report);
tracing::info!("{}", report);
}
}
match total_issues {
0 => println!("No issues found."),
1 => println!("1 issue found."),
_ => println!("{} issues found.", total_issues),
}
match total_issues {
0 => exit(0),
_ => exit(1),
0 => {
tracing::info!("No issues found.");
Ok(())
}
1 => Err("1 issue found.".into()),
_ => Err(format!("{} issues found.", total_issues).into()),
}
}
@@ -114,7 +113,6 @@ fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
};
// load the gitignore
for entry in folder {
let Ok(entry) = entry else {
continue;
+7 -6
View File
@@ -1,25 +1,26 @@
use super::*;
use crate::Result;
/// Clean build artifacts.
///
/// Simlpy runs `cargo clean`
#[derive(Clone, Debug, Parser)]
#[clap(name = "clean")]
pub(crate) struct Clean {}
impl Clean {
/// todo(jon): we should add a config option that just wipes target/dx and target/dioxus-client instead of doing a full clean
pub(crate) fn clean(self) -> anyhow::Result<()> {
let output = Command::new("cargo")
pub(crate) async fn clean(self) -> Result<StructuredOutput> {
let output = tokio::process::Command::new("cargo")
.arg("clean")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()?;
.output()
.await?;
if !output.status.success() {
return Err(anyhow::anyhow!("Cargo clean failed."));
return Err(anyhow::anyhow!("Cargo clean failed.").into());
}
Ok(())
Ok(StructuredOutput::Success)
}
}
+6 -6
View File
@@ -4,7 +4,6 @@ use crate::{metadata::crate_root, CliSettings};
/// Dioxus config file controls
#[derive(Clone, Debug, Deserialize, Subcommand)]
#[clap(name = "config")]
pub(crate) enum Config {
/// Init `Dioxus.toml` for project/folder.
Init {
@@ -76,7 +75,7 @@ impl From<BoolValue> for bool {
}
impl Config {
pub(crate) fn config(self) -> Result<()> {
pub(crate) fn config(self) -> Result<StructuredOutput> {
let crate_root = crate_root()?;
match self {
Config::Init {
@@ -89,7 +88,7 @@ impl Config {
tracing::warn!(
"config file `Dioxus.toml` already exist, use `--force` to overwrite it."
);
return Ok(());
return Ok(StructuredOutput::Success);
}
let mut file = File::create(conf_path)?;
let content = String::from(include_str!("../../assets/dioxus.toml"))
@@ -99,7 +98,7 @@ impl Config {
tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 Init config file completed.");
}
Config::FormatPrint {} => {
println!(
tracing::info!(
"{:#?}",
crate::dioxus_crate::DioxusCrate::new(&TargetArgs::default())?.config
);
@@ -112,7 +111,7 @@ impl Config {
tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 Create custom html file done.");
}
Config::LogFile {} => {
let log_path = crate::tracer::log_path();
let log_path = crate::logging::FileAppendLayer::log_path();
tracing::info!(dx_src = ?TraceSrc::Dev, "Log file is located at {}", log_path.display());
}
// Handle CLI settings.
@@ -132,6 +131,7 @@ impl Config {
tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 CLI setting `{setting}` has been set.");
}
}
Ok(())
Ok(StructuredOutput::Success)
}
}
+4 -3
View File
@@ -7,7 +7,6 @@ use std::path::Path;
pub(crate) static DEFAULT_TEMPLATE: &str = "gh:dioxuslabs/dioxus-template";
#[derive(Clone, Debug, Default, Deserialize, Parser)]
#[clap(name = "new")]
pub(crate) struct Create {
/// Project name (required when `--yes` is used)
name: Option<String>,
@@ -39,7 +38,7 @@ pub(crate) struct Create {
}
impl Create {
pub(crate) fn create(mut self) -> Result<()> {
pub(crate) fn create(mut self) -> Result<StructuredOutput> {
let metadata = cargo_metadata::MetadataCommand::new().exec().ok();
// If we're getting pass a `.` name, that's actually a path
@@ -106,7 +105,9 @@ impl Create {
.expect("ctrlc::set_handler");
let path = cargo_generate::generate(args)?;
post_create(&path, metadata)
post_create(&path, metadata)?;
Ok(StructuredOutput::Success)
}
}
+3 -2
View File
@@ -1,10 +1,11 @@
use crate::{Result, StructuredOutput};
use clap::Parser;
#[derive(Clone, Debug, Parser)]
pub struct Doctor {}
impl Doctor {
pub async fn run(self) -> anyhow::Result<()> {
Ok(())
pub async fn run(self) -> Result<StructuredOutput> {
Ok(StructuredOutput::Success)
}
}
+7 -6
View File
@@ -3,28 +3,27 @@ use crate::cli::create::DEFAULT_TEMPLATE;
use cargo_generate::{GenerateArgs, TemplatePath};
#[derive(Clone, Debug, Default, Deserialize, Parser)]
#[clap(name = "init")]
pub(crate) struct Init {
/// Template path
#[clap(default_value = DEFAULT_TEMPLATE, short, long)]
template: String,
/// Pass <option>=<value> for the used template (e.g., `foo=bar`)
#[clap(short, long)]
option: Vec<String>,
/// Specify a sub-template within the template repository to be used as the actual template
#[clap(long)]
subtemplate: Option<String>,
/// Skip user interaction by using the default values for the used template.
/// Default values can be overridden with `--option`
#[clap(short, long)]
yes: bool,
// TODO: turn on/off cargo-generate's output (now is invisible)
// #[clap(default_value = "false", short, long)]
// silent: bool,
}
impl Init {
pub(crate) fn init(self) -> Result<()> {
pub(crate) fn init(self) -> Result<StructuredOutput> {
let metadata = cargo_metadata::MetadataCommand::new().exec().ok();
// Get directory name.
@@ -57,6 +56,8 @@ impl Init {
..Default::default()
};
let path = cargo_generate::generate(args)?;
create::post_create(&path, metadata)
create::post_create(&path, metadata)?;
Ok(StructuredOutput::Success)
}
}
+3 -6
View File
@@ -1,8 +1,7 @@
use crate::assets::AssetManifest;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::assets::AssetManifest;
#[derive(Debug, Serialize, Deserialize)]
pub enum LinkAction {
BuildAssetManifest {
@@ -15,7 +14,7 @@ pub enum LinkAction {
}
impl LinkAction {
pub(crate) const ENV_VAR_NAME: &'static str = "dx-magic-link-file";
pub(crate) const ENV_VAR_NAME: &'static str = "dx_magic_link_file";
/// Should we write the input arguments to a file (aka act as a linker subprocess)?
///
@@ -39,7 +38,7 @@ impl LinkAction {
///
/// hmmmmmmmm tbh I'd rather just pass the object files back and do the parsing here, but the interface
/// is nicer to just bounce back the args and let the host do the parsing/canonicalization
pub(crate) fn run(self) -> anyhow::Result<()> {
pub(crate) fn run(self) {
match self {
// Literally just run the android linker :)
LinkAction::LinkAndroid {
@@ -102,7 +101,5 @@ impl LinkAction {
std::fs::write(dest, contents).expect("Failed to write output file");
}
}
Ok(())
}
}
+16 -11
View File
@@ -12,12 +12,14 @@ pub(crate) mod run;
pub(crate) mod serve;
pub(crate) mod target;
pub(crate) mod translate;
pub(crate) mod verbosity;
pub(crate) use build::*;
pub(crate) use serve::*;
pub(crate) use target::*;
pub(crate) use verbosity::*;
use crate::{error::Result, Error};
use crate::{error::Result, Error, StructuredOutput};
use anyhow::Context;
use clap::{Parser, Subcommand};
use html_parser::Dom;
@@ -35,40 +37,42 @@ use std::{
#[derive(Parser)]
#[clap(name = "dioxus", version = VERSION.as_str())]
pub(crate) struct Cli {
#[clap(subcommand)]
#[command(subcommand)]
pub(crate) action: Commands,
/// Enable verbose logging.
#[clap(short)]
pub(crate) v: bool,
/// Specify a binary target.
#[clap(global = true, long)]
pub(crate) bin: Option<String>,
#[command(flatten)]
pub(crate) verbosity: Verbosity,
}
#[derive(Parser)]
#[derive(Subcommand)]
pub(crate) enum Commands {
/// Build the Dioxus project and all of its assets.
#[clap(name = "build")]
Build(build::BuildArgs),
/// Translate a source file into Dioxus code.
#[clap(name = "translate")]
Translate(translate::Translate),
/// Build, watch & serve the Dioxus project and all of its assets.
#[clap(name = "serve")]
Serve(serve::ServeArgs),
/// Create a new project for Dioxus.
#[clap(name = "new")]
New(create::Create),
/// Init a new project for Dioxus in an existing directory.
/// Will attempt to keep your project in a good state.
#[clap(name = "init")]
Init(init::Init),
/// Clean output artifacts.
#[clap(name = "clean")]
Clean(clean::Clean),
/// Bundle the Dioxus app into a shippable object.
#[clap(name = "bundle")]
Bundle(bundle::Bundle),
/// Automatically format RSX.
@@ -89,6 +93,7 @@ pub(crate) enum Commands {
/// Dioxus config file controls.
#[clap(subcommand)]
#[clap(name = "config")]
Config(config::Config),
}
@@ -111,7 +116,7 @@ impl Display for Commands {
}
}
pub(crate) static VERSION: Lazy<String> = Lazy::new(|| {
static VERSION: Lazy<String> = Lazy::new(|| {
format!(
"{} ({})",
crate::dx_build_info::PKG_VERSION,
+12 -8
View File
@@ -1,5 +1,5 @@
use super::*;
use crate::{serve::ServeUpdate, BuildArgs, Builder, DioxusCrate};
use crate::{serve::ServeUpdate, BuildArgs, Builder, DioxusCrate, Result};
/// Run the project with the given arguments
#[derive(Clone, Debug, Parser)]
@@ -10,14 +10,14 @@ pub(crate) struct RunArgs {
}
impl RunArgs {
pub(crate) async fn run(mut self) -> anyhow::Result<()> {
pub(crate) async fn run(mut self) -> Result<StructuredOutput> {
let krate = DioxusCrate::new(&self.build_args.target_args)
.context("Failed to load Dioxus workspace")?;
self.build_args.resolve(&krate)?;
println!("Building crate krate data: {:#?}", krate);
println!("Build args: {:#?}", self.build_args);
tracing::trace!("Building crate krate data: {:#?}", krate);
tracing::trace!("Build args: {:#?}", self.build_args);
let bundle = Builder::start(&krate, self.build_args.clone())?
.finish()
@@ -35,11 +35,15 @@ impl RunArgs {
// They won't generally be emitted
loop {
match runner.wait().await {
ServeUpdate::StderrReceived { platform, msg } => println!("[{platform}]: {msg}"),
ServeUpdate::StdoutReceived { platform, msg } => println!("[{platform}]: {msg}"),
ServeUpdate::StderrReceived { platform, msg } => {
tracing::info!("[{platform}]: {msg}")
}
ServeUpdate::StdoutReceived { platform, msg } => {
tracing::info!("[{platform}]: {msg}")
}
ServeUpdate::ProcessExited { platform, status } => {
runner.kill(platform);
eprintln!("[{platform}]: process exited with status: {status:?}");
tracing::info!("[{platform}]: process exited with status: {status:?}");
break;
}
ServeUpdate::BuildUpdate { .. } => {}
@@ -55,6 +59,6 @@ impl RunArgs {
}
}
Ok(())
Ok(StructuredOutput::Success)
}
}
+17 -7
View File
@@ -1,11 +1,9 @@
use super::*;
use crate::{AddressArguments, BuildArgs, DioxusCrate, Platform};
use anyhow::Context;
/// Serve the project
#[derive(Clone, Debug, Default, Parser)]
#[command(group = clap::ArgGroup::new("release-incompatible").multiple(true).conflicts_with("release"))]
#[clap(name = "serve")]
pub(crate) struct ServeArgs {
/// The arguments for the address the server will run on
#[clap(flatten)]
@@ -47,10 +45,22 @@ pub(crate) struct ServeArgs {
impl ServeArgs {
/// Start the tui, builder, etc by resolving the arguments and then running the actual top-level serve function
pub(crate) async fn serve(mut self) -> Result<()> {
let krate = DioxusCrate::new(&self.build_arguments.target_args)
.context("Failed to load Dioxus workspace")?;
///
/// Make sure not to do any intermediate logging since our tracing infra has now enabled much
/// higher log levels
pub(crate) async fn serve(self) -> Result<StructuredOutput> {
crate::serve::serve_all(self).await?;
Ok(StructuredOutput::Success)
}
pub(crate) fn load_krate(&mut self) -> Result<DioxusCrate> {
let krate = DioxusCrate::new(&self.build_arguments.target_args)?;
self.resolve(&krate)?;
Ok(krate)
}
pub(crate) fn resolve(&mut self, krate: &DioxusCrate) -> Result<()> {
// Enable hot reload.
if self.hot_reload.is_none() {
self.hot_reload = Some(krate.settings.always_hot_reload.unwrap_or(true));
@@ -72,9 +82,9 @@ impl ServeArgs {
}
// Resolve the build arguments
self.build_arguments.resolve(&krate)?;
self.build_arguments.resolve(krate)?;
crate::serve::serve_all(self, krate).await
Ok(())
}
pub(crate) fn should_hotreload(&self) -> bool {
+12 -10
View File
@@ -1,11 +1,9 @@
use super::*;
use crate::{Result, TraceSrc};
use crate::{Result, StructuredOutput};
use dioxus_rsx::{BodyNode, CallBody, TemplateBody};
use std::{io::IsTerminal as _, process::exit};
/// Translate some source file into Dioxus code
#[derive(Clone, Debug, Parser)]
#[clap(name = "translate")]
pub(crate) struct Translate {
/// Activate debug mode
// short and long flags (-d, --debug) will be deduced from the field's name
@@ -26,7 +24,7 @@ pub(crate) struct Translate {
}
impl Translate {
pub(crate) fn translate(self) -> Result<()> {
pub(crate) fn translate(self) -> Result<StructuredOutput> {
// Get the right input for the translation
let contents = determine_input(self.file, self.raw)?;
@@ -34,15 +32,18 @@ impl Translate {
let dom = html_parser::Dom::parse(&contents)?;
// Convert the HTML to RSX
let out = convert_html_to_formatted_rsx(&dom, self.component);
let html = convert_html_to_formatted_rsx(&dom, self.component);
// Write the output
// todo(jon): we should probably use tracing out a different output format
// right now we're just printing to stdout since some tools rely on that, but likely we don't want that
// instead we should be printing as json (or maybe even a different format) if we're not interactive
match self.output {
Some(output) => std::fs::write(output, out)?,
None => print!("{}", out),
Some(output) => std::fs::write(output, &html)?,
None => print!("{}", html),
}
Ok(())
Ok(StructuredOutput::HtmlTranslate { html })
}
}
@@ -103,10 +104,11 @@ fn indent_and_write(raw: &str, idx: usize, out: &mut String) {
}
fn determine_input(file: Option<String>, raw: Option<String>) -> Result<String> {
use std::io::IsTerminal as _;
// Make sure not both are specified
if file.is_some() && raw.is_some() {
tracing::error!(dx_src = ?TraceSrc::Dev, "Only one of --file or --raw should be specified.");
exit(0);
return Err("Only one of --file or --raw should be specified.".into());
}
if let Some(raw) = raw {
+16
View File
@@ -0,0 +1,16 @@
use clap::Parser;
#[derive(Parser, Clone, Copy, Debug)]
pub struct Verbosity {
/// Use verbose output [default: false]
#[clap(long, global = true)]
pub(crate) verbose: bool,
/// Use trace output [default: false]
#[clap(long, global = true)]
pub(crate) trace: bool,
/// Output logs in JSON format
#[clap(long, global = true)]
pub(crate) json_output: bool,
}
+33 -13
View File
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::{collections::HashMap, str::FromStr};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub(crate) struct BundleConfig {
@@ -181,18 +181,6 @@ impl Default for WebviewInstallMode {
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum PackageType {
MacOsBundle,
IosBundle,
WindowsMsi,
Deb,
Rpm,
AppImage,
Dmg,
Updater,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomSignCommandSettings {
/// The command to run to sign the binary.
@@ -202,3 +190,35 @@ pub struct CustomSignCommandSettings {
/// "%1" will be replaced with the path to the binary to be signed.
pub args: Vec<String>,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum PackageType {
MacOsBundle,
IosBundle,
WindowsMsi,
Nsis,
Deb,
Rpm,
AppImage,
Dmg,
Updater,
}
impl FromStr for PackageType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"macos" => Ok(PackageType::MacOsBundle),
"ios" => Ok(PackageType::IosBundle),
"msi" => Ok(PackageType::WindowsMsi),
"nsis" => Ok(PackageType::Nsis),
"deb" => Ok(PackageType::Deb),
"rpm" => Ok(PackageType::Rpm),
"appimage" => Ok(PackageType::AppImage),
"dmg" => Ok(PackageType::Dmg),
"updater" => Ok(PackageType::Updater),
_ => Err(format!("{} is not a valid package type", s)),
}
}
}
+63 -54
View File
@@ -25,14 +25,15 @@ pub(crate) static PROFILE_SERVER: &str = "dioxus-server";
impl DioxusCrate {
pub(crate) fn new(target: &TargetArgs) -> Result<Self> {
let mut cmd = Cmd::new();
cmd.features(target.features.clone());
let krates = krates::Builder::new()
tracing::debug!("Loading crate");
let cmd = Cmd::new();
let builder = krates::Builder::new();
let krates = builder
.build(cmd, |_| {})
.context("Failed to run cargo metadata")?;
let package = find_main_package(&krates, target.package.clone())?;
tracing::debug!("Found package {package:?}");
let dioxus_config = DioxusConfig::load(&krates, package)?.unwrap_or_default();
@@ -173,6 +174,7 @@ impl DioxusCrate {
.get_enabled_features(krate.kid)?
.iter()
.flat_map(|feature| {
tracing::trace!("Autodetecting platform from feature {feature}");
Platform::autodetect_from_cargo_feature(feature).map(|f| (f, feature.to_string()))
})
.collect::<Vec<_>>();
@@ -230,8 +232,6 @@ impl DioxusCrate {
return possible_platforms.first().cloned();
}
tracing::warn!("Could not autodetect platform. Platform must be explicitly specified. Pass `--platform <platform>` or set a default platform using a cargo feature.");
None
}
@@ -246,13 +246,13 @@ impl DioxusCrate {
}
/// Get the features required to build for the given platform
pub(crate) fn feature_for_platform(&self, platform: Platform) -> Option<String> {
pub(crate) fn feature_for_platform(&self, platform: Platform) -> String {
let package = self.package();
// Try to find the feature that activates the dioxus feature for the given platform
let dioxus_feature = platform.feature_name();
package.features.iter().find_map(|(key, features)| {
let res = package.features.iter().find_map(|(key, features)| {
// if the feature is just the name of the platform, we use that
if key == dioxus_feature {
return Some(key.clone());
@@ -271,7 +271,16 @@ impl DioxusCrate {
}
}
}
None
});
res.unwrap_or_else(|| {
let fallback = format!("dioxus/{}", platform.feature_name()) ;
tracing::debug!(
"Could not find explicit feature for platform {platform}, passing `fallback` instead"
);
fallback
})
}
@@ -312,14 +321,13 @@ impl DioxusCrate {
if let toml_edit::Entry::Vacant(entry) = table.entry(PROFILE_SERVER) {
let mut server = toml_edit::Table::new();
server.insert("inherits", Item::Value("dev".into()));
server.insert("opt-level", Item::Value(2.into()));
// server.insert("opt-level", Item::Value(2.into()));
entry.insert(Item::Table(server));
}
if let toml_edit::Entry::Vacant(entry) = table.entry(PROFILE_ANDROID) {
let mut android = toml_edit::Table::new();
android.insert("inherits", Item::Value("dev".into()));
android.insert("opt-level", Item::Value(2.into()));
entry.insert(Item::Table(android));
}
}
@@ -561,59 +569,60 @@ impl std::fmt::Debug for DioxusCrate {
// Find the main package in the workspace
fn find_main_package(krates: &Krates, package: Option<String>) -> Result<NodeId> {
let kid = match package {
Some(package) => {
let mut workspace_members = krates.workspace_members();
let found = workspace_members.find_map(|node| {
if let krates::Node::Krate { id, krate, .. } = node {
if krate.name == package {
return Some(id);
}
}
None
});
if found.is_none() {
eprintln!("Could not find package {package} in the workspace. Did you forget to add it to the workspace?");
eprintln!("Packages in the workspace:");
for package in krates.workspace_members() {
if let krates::Node::Krate { krate, .. } = package {
eprintln!("{}", krate.name());
}
if let Some(package) = package {
let mut workspace_members = krates.workspace_members();
let found = workspace_members.find_map(|node| {
if let krates::Node::Krate { id, krate, .. } = node {
if krate.name == package {
return Some(id);
}
}
None
});
found.ok_or_else(|| anyhow::anyhow!("Failed to find package {package}"))?
if found.is_none() {
tracing::error!("Could not find package {package} in the workspace. Did you forget to add it to the workspace?");
tracing::error!("Packages in the workspace:");
for package in krates.workspace_members() {
if let krates::Node::Krate { krate, .. } = package {
tracing::error!("{}", krate.name());
}
}
}
None => {
// Otherwise find the package that is the closest parent of the current directory
let current_dir = std::env::current_dir()?;
let current_dir = current_dir.as_path();
// Go through each member and find the path that is a parent of the current directory
let mut closest_parent = None;
for member in krates.workspace_members() {
if let krates::Node::Krate { id, krate, .. } = member {
let member_path = krate.manifest_path.parent().unwrap();
if let Ok(path) = current_dir.strip_prefix(member_path.as_std_path()) {
let len = path.components().count();
match closest_parent {
Some((_, closest_parent_len)) => {
if len < closest_parent_len {
closest_parent = Some((id, len));
}
}
None => {
closest_parent = Some((id, len));
}
let kid = found.ok_or_else(|| anyhow::anyhow!("Failed to find package {package}"))?;
return Ok(krates.nid_for_kid(kid).unwrap());
};
// Otherwise find the package that is the closest parent of the current directory
let current_dir = std::env::current_dir()?;
let current_dir = current_dir.as_path();
// Go through each member and find the path that is a parent of the current directory
let mut closest_parent = None;
for member in krates.workspace_members() {
if let krates::Node::Krate { id, krate, .. } = member {
let member_path = krate.manifest_path.parent().unwrap();
if let Ok(path) = current_dir.strip_prefix(member_path.as_std_path()) {
let len = path.components().count();
match closest_parent {
Some((_, closest_parent_len)) => {
if len < closest_parent_len {
closest_parent = Some((id, len));
}
}
None => {
closest_parent = Some((id, len));
}
}
}
closest_parent
.map(|(id, _)| id)
.context("Failed to find current package")?
}
};
}
let kid = closest_parent
.map(|(id, _)| id)
.context("Failed to find current package")?;
let package = krates.nid_for_kid(kid).unwrap();
Ok(package)
+9
View File
@@ -30,6 +30,15 @@ pub(crate) enum Error {
#[error("Failed to establish proxy: {0}")]
ProxySetup(String),
#[error("Failed to bundle project: {0}")]
BundleFailed(#[from] tauri_bundler::Error),
#[error("Unsupported feature: {0}")]
UnsupportedFeature(String),
#[error("Failed to render template: {0}")]
TemplateParse(#[from] handlebars::RenderError),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
@@ -14,8 +14,9 @@
//! 3. Build CLI layer for routing tracing logs to the TUI.
//! 4. Build fmt layer for non-interactive logging with a custom writer that prevents output during interactive mode.
use crate::{serve::ServeUpdate, Platform as TargetPlatform};
use crate::{serve::ServeUpdate, Cli, Commands, Platform as TargetPlatform, Verbosity};
use cargo_metadata::{diagnostic::DiagnosticLevel, CompilerMessage};
use clap::Parser;
use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
use once_cell::sync::OnceCell;
use std::{
@@ -23,37 +24,107 @@ use std::{
env,
fmt::{Debug, Display, Write as _},
fs,
io::{self, Write},
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Mutex,
},
sync::Mutex,
time::Instant,
};
use tracing::{field::Visit, Level, Subscriber};
use tracing_subscriber::{fmt::format, prelude::*, registry::LookupSpan, EnvFilter, Layer};
use tracing_subscriber::{
fmt::{
format::{self, Writer},
time::FormatTime,
},
prelude::*,
registry::LookupSpan,
EnvFilter, Layer,
};
const LOG_ENV: &str = "DIOXUS_LOG";
const LOG_FILE_NAME: &str = "dx.log";
const DX_SRC_FLAG: &str = "dx_src";
pub fn log_path() -> PathBuf {
let tmp_dir = std::env::temp_dir();
tmp_dir.join(LOG_FILE_NAME)
}
static TUI_ENABLED: AtomicBool = AtomicBool::new(false);
static TUI_TX: OnceCell<UnboundedSender<TraceMsg>> = OnceCell::new();
pub static VERBOSITY: OnceCell<Verbosity> = OnceCell::new();
pub(crate) struct TraceController {
pub(crate) tui_rx: UnboundedReceiver<TraceMsg>,
}
impl TraceController {
/// Initialize the CLI and set up the tracing infrastructure
pub fn initialize() -> Cli {
let args = Cli::parse();
VERBOSITY
.set(args.verbosity)
.expect("verbosity should only be set once");
// When running in interactive mode (of which serve is the only one), we want to do things slightly differently
// This involves no fmt layer or file logging
if matches!(args.action, Commands::Serve(_)) {
Self::initialize_for_serve();
return args;
}
// By default we capture ourselves at a higher tracing level when serving
// This ensures we're tracing ourselves even if we end up tossing the logs
let filter = if env::var(LOG_ENV).is_ok() {
EnvFilter::from_env(LOG_ENV)
} else {
EnvFilter::new(format!(
"error,dx={our_level},dioxus-cli={our_level},manganis-cli-support={our_level}",
our_level = if args.verbosity.verbose {
"debug"
} else {
"info"
}
))
};
let json_filter = tracing_subscriber::filter::filter_fn(move |meta| {
if meta.fields().len() == 1 && meta.fields().iter().next().unwrap().name() == "json" {
return args.verbosity.json_output;
}
true
});
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(args.verbosity.verbose)
.fmt_fields(
format::debug_fn(move |writer, field, value| {
if field.name() == "json" && !args.verbosity.json_output {
return Ok(());
}
write!(writer, "{}", format_field(field.name(), value))
})
.delimited(" "),
)
.with_timer(PrettyUptime::default());
let fmt_layer = if args.verbosity.json_output {
fmt_layer.json().flatten_event(true).boxed()
} else {
fmt_layer.boxed()
};
let sub = tracing_subscriber::registry()
.with(filter)
.with(json_filter)
.with(FileAppendLayer::new())
.with(fmt_layer);
#[cfg(feature = "tokio-console")]
let sub = sub.with(console_subscriber::spawn());
sub.init();
args
}
/// Get a handle to the trace controller.
pub fn redirect() -> Self {
let (tui_tx, tui_rx) = unbounded();
TUI_ENABLED.store(true, Ordering::SeqCst);
TUI_TX.set(tui_tx.clone()).unwrap();
Self { tui_rx }
}
@@ -65,54 +136,13 @@ impl TraceController {
ServeUpdate::TracingLog { log }
}
pub(crate) fn shutdown(&self) {
TUI_ENABLED.store(false, Ordering::SeqCst);
}
fn initialize_for_serve() {
let filter = EnvFilter::new("error,dx=trace,dioxus-cli=trace,manganis-cli-support=trace");
/// Build tracing infrastructure.
pub fn initialize() {
let mut filter =
EnvFilter::new("error,dx=trace,dioxus-cli=debug,manganis-cli-support=debug");
if env::var(LOG_ENV).is_ok() {
filter = EnvFilter::from_env(LOG_ENV);
}
// Log file
let log_path = log_path();
_ = std::fs::write(&log_path, "");
let file_append_layer = match FileAppendLayer::new(log_path) {
Ok(f) => Some(f),
Err(e) => {
tracing::error!(dx_src = ?TraceSrc::Dev, err = ?e, "failed to init log file");
None
}
};
// Build CLI layer
let cli_layer = CLILayer;
// Build fmt layer
let fmt_layer = tracing_subscriber::fmt::layer()
.fmt_fields(
format::debug_fn(|writer, field, value| {
write!(writer, "{}", format_field(field.name(), value))
})
.delimited(" "),
)
.with_writer(Mutex::new(FmtLogWriter {}))
.with_timer(tracing_subscriber::fmt::time::time());
let sub = tracing_subscriber::registry()
tracing_subscriber::registry()
.with(filter)
.with(file_append_layer)
.with(cli_layer)
.with(fmt_layer);
#[cfg(feature = "tokio-console")]
let sub = sub.with(console_subscriber::spawn());
sub.init();
.with(CLILayer {})
.init();
}
}
@@ -120,17 +150,27 @@ impl TraceController {
///
/// This layer returns on any error allowing the cli to continue work
/// despite failing to log to a file. This helps in case of permission errors and similar.
struct FileAppendLayer {
pub(crate) struct FileAppendLayer {
file_path: PathBuf,
buffer: Mutex<String>,
}
impl FileAppendLayer {
pub fn new(file_path: PathBuf) -> io::Result<Self> {
Ok(Self {
fn new() -> Self {
let file_path = Self::log_path();
if !file_path.exists() {
_ = std::fs::write(&file_path, "");
}
Self {
file_path,
buffer: Mutex::new(String::new()),
})
}
}
pub(crate) fn log_path() -> PathBuf {
std::env::temp_dir().join(LOG_FILE_NAME)
}
}
@@ -194,15 +234,6 @@ where
let mut visitor = CollectVisitor::new();
event.record(&mut visitor);
// If the TUI output is disabled we let fmt subscriber handle the logs
// EXCEPT for cargo logs which we just print.
if !TUI_ENABLED.load(Ordering::SeqCst) {
if visitor.source == TraceSrc::Cargo {
println!("{}", visitor.message);
}
return;
}
let meta = event.metadata();
let level = meta.level();
@@ -217,14 +248,11 @@ where
visitor.source = TraceSrc::Dev;
}
TUI_TX
_ = TUI_TX
.get()
.unwrap()
.unbounded_send(TraceMsg::text(visitor.source, *level, final_msg))
.unwrap();
.unbounded_send(TraceMsg::text(visitor.source, *level, final_msg));
}
// TODO: support spans? structured tui log display?
}
/// A record visitor that collects dx-specific info and user-provided fields for logging consumption.
@@ -239,7 +267,6 @@ impl CollectVisitor {
Self {
message: String::new(),
source: TraceSrc::Unknown,
fields: HashMap::new(),
}
}
@@ -266,26 +293,6 @@ impl Visit for CollectVisitor {
}
}
struct FmtLogWriter {}
impl Write for FmtLogWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if !TUI_ENABLED.load(Ordering::SeqCst) {
return std::io::stdout().write(buf);
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
if !TUI_ENABLED.load(Ordering::SeqCst) {
std::io::stdout().flush()?;
}
Ok(())
}
}
/// Formats a tracing field and value, removing any internal fields from the final output.
fn format_field(field_name: &str, value: &dyn Debug) -> String {
let mut out = String::new();
@@ -398,3 +405,26 @@ impl Display for TraceSrc {
}
}
}
/// Retrieve and print the relative elapsed wall-clock time since an epoch.
///
/// The `Default` implementation for `Uptime` makes the epoch the current time.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct PrettyUptime {
epoch: Instant,
}
impl Default for PrettyUptime {
fn default() -> Self {
Self {
epoch: Instant::now(),
}
}
}
impl FormatTime for PrettyUptime {
fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
let e = self.epoch.elapsed();
write!(w, "{:4}.{:2}s", e.as_secs(), e.subsec_millis())
}
}
+37 -37
View File
@@ -4,7 +4,7 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
mod assets;
mod builder;
mod build;
mod bundle_utils;
mod cli;
mod config;
@@ -13,65 +13,65 @@ mod dx_build_info;
mod error;
mod fastfs;
mod filemap;
mod logging;
mod metadata;
mod platform;
mod profiles;
mod rustup;
mod serve;
mod settings;
mod slog;
mod tooling;
mod tracer;
pub(crate) use builder::*;
pub(crate) use build::*;
pub(crate) use cli::*;
pub(crate) use config::*;
pub(crate) use dioxus_crate::*;
pub(crate) use dioxus_dx_wire_format::*;
pub(crate) use error::*;
pub(crate) use filemap::*;
pub(crate) use logging::*;
pub(crate) use platform::*;
pub(crate) use rustup::*;
pub(crate) use settings::*;
pub(crate) use tracer::*;
use anyhow::Context;
use clap::Parser;
use Commands::*;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
async fn main() {
// If we're being ran as a linker (likely from ourselves), we want to act as a linker instead.
if let Some(link_action) = link::LinkAction::from_env() {
return link_action.run();
}
// Start the tracer so it captures logs from the build engine before we start the builder
TraceController::initialize();
let args = TraceController::initialize();
let result = match args.action {
Commands::Translate(opts) => opts.translate(),
Commands::New(opts) => opts.create(),
Commands::Init(opts) => opts.init(),
Commands::Config(opts) => opts.config(),
Commands::Autoformat(opts) => opts.autoformat(),
Commands::Check(opts) => opts.check().await,
Commands::Clean(opts) => opts.clean().await,
Commands::Build(opts) => opts.run_cmd().await,
Commands::Serve(opts) => opts.serve().await,
Commands::Bundle(opts) => opts.bundle().await,
Commands::Run(opts) => opts.run().await,
Commands::Doctor(opts) => opts.run().await,
};
match Cli::parse().action {
Translate(opts) => opts
.translate()
.context("⛔️ Translation of HTML into RSX failed:"),
// Provide a structured output for third party tools that can consume the output of the CLI
match result {
Ok(output) => {
tracing::debug!(json = ?output);
}
Err(err) => {
tracing::error!(
?err,
json = ?StructuredOutput::Error {
message: format!("{err:?}"),
},
);
New(opts) => opts.create().context("🚫 Creating new project failed:"),
Init(opts) => opts.init().context("🚫 Initializing a new project failed:"),
Config(opts) => opts.config().context("🚫 Configuring new project failed:"),
Autoformat(opts) => opts.autoformat().context("🚫 Error autoformatting RSX:"),
Check(opts) => opts.check().await.context("🚫 Error checking RSX:"),
Clean(opts) => opts.clean().context("🚫 Cleaning project failed:"),
Build(mut opts) => opts.build_it().await.context("🚫 Building project failed:"),
Serve(opts) => opts.serve().await.context("🚫 Serving project failed:"),
Bundle(opts) => opts.bundle().await.context("🚫 Bundling project failed:"),
Run(opts) => opts.run().await.context("🚫 Running project failed:"),
Doctor(opts) => opts.run().await.context("🚫 Checking project failed:"),
}
std::process::exit(1);
}
};
}
+1 -4
View File
@@ -174,10 +174,7 @@ impl Platform {
Some(Platform::Linux)
}
}
"mobile" => {
tracing::warn!("Could not autodetect mobile platform. Mobile platforms must be explicitly specified. Pass `--platform ios` or `--platform android` instead.");
None
}
"mobile" => None,
"liveview" => Some(Platform::Liveview),
"server" => Some(Platform::Server),
_ => None,
+38 -8
View File
@@ -68,6 +68,7 @@ impl AppHandle {
// These need to be stable within a release version (ie 0.6.0)
let mut envs = vec![
("DIOXUS_CLI_ENABLED", "true".to_string()),
("RUST_BACKTRACE", "1".to_string()),
(
dioxus_cli_config::DEVSERVER_RAW_ADDR_ENV,
devserver_ip.to_string(),
@@ -116,7 +117,7 @@ impl AppHandle {
// https://developer.android.com/studio/run/emulator-commandline
Platform::Android => {
tracing::error!("Android is not yet supported, sorry!");
self.open_android_sim(envs).await?;
None
}
@@ -159,7 +160,7 @@ impl AppHandle {
// we won't actually be using the build dir.
let asset_dir = match self.runtime_asst_dir.as_ref() {
Some(dir) => dir.to_path_buf().join("assets/"),
None => self.app.asset_dir(),
None => self.app.build.asset_dir(),
};
tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
@@ -237,13 +238,16 @@ impl AppHandle {
/// TODO(jon): we should probably check if there's a simulator running before trying to install,
/// and open the simulator if we have to.
async fn open_ios_sim(&mut self, envs: Vec<(&str, String)>) -> Result<Child> {
tracing::debug!("Installing app to simulator {:?}", self.app.app_dir());
tracing::debug!(
"Installing app to simulator {:?}",
self.app.build.root_dir()
);
let res = Command::new("xcrun")
.arg("simctl")
.arg("install")
.arg("booted")
.arg(self.app.app_dir())
.arg(self.app.build.root_dir())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.output()
@@ -262,15 +266,13 @@ impl AppHandle {
.arg("launch")
.arg("--console")
.arg("booted")
.arg("com.dioxuslabs")
.arg(self.app.bundle_identifier())
.envs(ios_envs)
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.kill_on_drop(true)
.spawn()?;
tracing::debug!("Launched app on simulator with exit code: {child:?}");
Ok(child)
}
@@ -309,7 +311,7 @@ impl AppHandle {
// # xcrun devicectl device process resume --device "${DEVICE_UUID}" --pid "${STATUS_PID}" > "${XCRUN_DEVICE_PROCESS_RESUME_LOG_DIR}" 2>&1
use serde_json::Value;
let app_path = self.app.app_dir();
let app_path = self.app.build.root_dir();
install_app(&app_path).await?;
@@ -448,4 +450,32 @@ impl AppHandle {
unimplemented!("dioxus-cli doesn't support ios devices yet.")
}
async fn open_android_sim(&self, envs: Vec<(&str, String)>) -> Result<()> {
// Install
// adb install -r app-debug.apk
let _output = Command::new("adb")
.arg("install")
.arg("-r")
.arg(self.app.apk_path())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.output()
.await?;
// adb shell am start -n com.example.androidfinal/com.example.androidfinal.MainActivity
let _output = Command::new("adb")
.arg("shell")
.arg("am")
.arg("start")
.arg("-n")
.arg("com.example.androidfinal/com.example.androidfinal.MainActivity")
.envs(envs)
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.output()
.await?;
Ok(())
}
}
+17 -16
View File
@@ -1,7 +1,4 @@
use crate::{
BuildUpdate, Builder, DioxusCrate, Error, Platform, Result, ServeArgs, TraceController,
TraceSrc,
};
use crate::{BuildUpdate, Builder, Error, Platform, Result, ServeArgs, TraceController, TraceSrc};
mod ansi_buffer;
mod detect;
@@ -40,9 +37,13 @@ pub(crate) use watcher::*;
/// - I'd love to be able to configure the CLI while it's running so we can change settings on the fly.
/// - I want us to be able to detect a `server_fn` in the project and then upgrade from a static server
/// to a dynamic one on the fly.
pub(crate) async fn serve_all(args: ServeArgs, krate: DioxusCrate) -> Result<()> {
pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
// Redirect all logging the cli logger
let mut tracer = TraceController::redirect();
// Load the krate and resolve the server args against it - this might log so do it after we turn on the tracer first
let krate = args.load_krate()?;
// Note that starting the builder will queue up a build immediately
let mut builder = Builder::start(&krate, args.build_args())?;
let mut devserver = WebServer::start(&krate, &args)?;
@@ -53,15 +54,15 @@ pub(crate) async fn serve_all(args: ServeArgs, krate: DioxusCrate) -> Result<()>
// This is our default splash screen. We might want to make this a fancier splash screen in the future
// Also, these commands might not be the most important, but it's all we've got enabled right now
tracing::info!(
r#"Serving your Dioxus app: {} 🚀
- Press `ctrl+c` to exit the server
- Press `r` to rebuild the app
- Press `o` to open the app
- Press `t` to toggle cargo output
- Press `/` for more commands and shortcuts
Learn more at https://dioxuslabs.com/learn/0.6/getting_started"#,
r#"-----------------------------------------------------------------
Serving your Dioxus app: {} 🚀
• Press `ctrl+c` to exit the server
• Press `r` to rebuild the app
• Press `o` to open the app
• Press `v` to toggle verbose logging
• Press `/` for more commands and shortcuts
Learn more at https://dioxuslabs.com/learn/0.6/getting_started
----------------------------------------------------------------"#,
krate.executable_name()
);
@@ -146,7 +147,7 @@ pub(crate) async fn serve_all(args: ServeArgs, krate: DioxusCrate) -> Result<()>
screen.new_build_update(&update);
// And then update the websocketed clients with the new build status in case they want it
devserver.new_build_update(&update).await;
devserver.new_build_update(&update, &builder).await;
// And then open the app if it's ready
// todo: there might be more things to do here that require coordination with other pieces of the CLI
@@ -213,6 +214,7 @@ pub(crate) async fn serve_all(args: ServeArgs, krate: DioxusCrate) -> Result<()>
// `Full rebuild:` to line up with
// `Hotreloading:` to keep the alignment during long edit sessions
tracing::info!("Full rebuild: triggered manually");
builder.rebuild(args.build_arguments.clone());
runner.file_map.force_rebuild();
devserver.start_build().await
@@ -248,7 +250,6 @@ pub(crate) async fn serve_all(args: ServeArgs, krate: DioxusCrate) -> Result<()>
_ = devserver.shutdown().await;
_ = screen.shutdown();
builder.abort_all();
tracer.shutdown();
if let Err(err) = err {
eprintln!("Exiting with error: {}", err);
+88 -105
View File
@@ -99,8 +99,8 @@ impl Output {
more_modal_open: false,
pending_logs: VecDeque::new(),
throbber: RefCell::new(throbber_widgets_tui::ThrobberState::default()),
trace: cfg.trace,
verbose: cfg.verbose,
trace: crate::logging::VERBOSITY.get().unwrap().trace,
verbose: crate::logging::VERBOSITY.get().unwrap().verbose,
tick_animation: false,
tick_interval: {
let mut interval = tokio::time::interval(Duration::from_millis(TICK_RATE_MS));
@@ -152,10 +152,8 @@ impl Output {
.execute(DisableBracketedPaste)?;
disable_raw_mode()?;
// print a few lines to not cut off the output
for _ in 0..3 {
println!();
}
// print a line to force the cursor down (no tearing)
println!();
}
Ok(())
@@ -331,10 +329,12 @@ impl Output {
/// we won't be drawing it.
pub(crate) fn new_build_update(&mut self, update: &BuildUpdate) {
match update {
BuildUpdate::CompilerMessage { .. } => {}
BuildUpdate::Progress { .. } => self.tick_animation = true,
BuildUpdate::Progress {
stage: BuildStage::Starting { .. },
} => self.tick_animation = true,
BuildUpdate::BuildReady { .. } => self.tick_animation = false,
BuildUpdate::BuildFailed { .. } => self.tick_animation = false,
_ => {}
}
}
@@ -381,7 +381,7 @@ impl Output {
let mut area = frame.area();
area.width = area.width.clamp(0, VIEWPORT_MAX_WIDTH);
let [_top, body, bottom] = Layout::vertical([
let [_top, body, _bottom] = Layout::vertical([
Constraint::Length(1),
Constraint::Fill(1),
Constraint::Length(1),
@@ -391,7 +391,6 @@ impl Output {
self.render_borders(frame, area);
self.render_body(frame, body, state);
self.render_bottom_row(frame, bottom, state);
self.render_body_title(frame, _top, state);
}
@@ -512,6 +511,7 @@ impl Output {
BuildStage::Failed => lines.push("Failed".red()),
BuildStage::Aborted => lines.push("Aborted".red()),
BuildStage::Restarting => lines.push("Restarting".yellow()),
_ => {}
};
frame.render_widget(Line::from(lines), status_line);
@@ -712,23 +712,6 @@ impl Output {
);
}
/// Render the version number on the bottom right
fn render_bottom_row(&self, frame: &mut Frame, area: Rect, _state: RenderState) {
// Split the area into two chunks
let row = Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).split(area);
frame.render_widget(
Paragraph::new(Line::from(vec![
// "🧬 dx".dark_gray(),
// " ".dark_gray(),
// self.dx_version.as_str().dark_gray(),
// " ".dark_gray(),
]))
.right_aligned(),
row[1],
);
}
/// Render borders around the terminal, forcing an inner clear while we're at it
fn render_borders(&self, frame: &mut Frame, area: Rect) {
frame.render_widget(ratatui::widgets::Clear, area);
@@ -784,7 +767,7 @@ impl Output {
// Render the log into an ansi string
// We're going to add some metadata to it like the timestamp and source and then dump it to the raw ansi sequences we need to send to crossterm
let output_sequence = tracemsg_to_ansi_string(log, term_size.width);
let output_sequence = Self::tracemsg_to_ansi_string(log, term_size.width);
// Get the lines of the output sequence and their overflow
let lines = output_sequence.lines().collect::<Vec<_>>();
@@ -863,94 +846,94 @@ impl Output {
false => VIEWPORT_HEIGHT_SMALL,
}
}
}
fn tracemsg_to_ansi_string(log: TraceMsg, term_width: u16) -> String {
let rendered = match log.content {
TraceContent::Cargo(msg) => msg.message.rendered.unwrap_or_default(),
TraceContent::Text(text) => text,
};
fn tracemsg_to_ansi_string(log: TraceMsg, term_width: u16) -> String {
let rendered = match log.content {
TraceContent::Cargo(msg) => msg.message.rendered.unwrap_or_default(),
TraceContent::Text(text) => text,
};
// Create a paragraph widget using the log line itself
// From here on out, we want to work with the escaped ansi string and the "real lines" to be printed
//
// We make a special case for lines that look like frames (ie ==== or ---- or ------) and make them
// dark gray, just for readability.
//
// todo(jon): refactor this out to accept any widget, not just paragraphs
let paragraph = Paragraph::new({
use ansi_to_tui::IntoText;
use chrono::Timelike;
// Create a paragraph widget using the log line itself
// From here on out, we want to work with the escaped ansi string and the "real lines" to be printed
//
// We make a special case for lines that look like frames (ie ==== or ---- or ------) and make them
// dark gray, just for readability.
//
// todo(jon): refactor this out to accept any widget, not just paragraphs
let paragraph = Paragraph::new({
use ansi_to_tui::IntoText;
use chrono::Timelike;
let mut text = Text::default();
let mut text = Text::default();
for (idx, raw_line) in rendered.lines().enumerate() {
let line_as_text = raw_line.into_text().unwrap();
let is_pretending_to_be_frame = raw_line
.chars()
.all(|c| c == '=' || c == '-' || c == ' ' || c == '─');
for (idx, raw_line) in rendered.lines().enumerate() {
let line_as_text = raw_line.into_text().unwrap();
let is_pretending_to_be_frame = raw_line
.chars()
.all(|c| c == '=' || c == '-' || c == ' ' || c == '─');
for (subline_idx, line) in line_as_text.lines.into_iter().enumerate() {
let mut out_line = if idx == 0 && subline_idx == 0 {
let mut formatted_line = Line::default();
for (subline_idx, line) in line_as_text.lines.into_iter().enumerate() {
let mut out_line = if idx == 0 && subline_idx == 0 {
let mut formatted_line = Line::default();
formatted_line.push_span(
Span::raw(format!(
"{:02}:{:02}:{:02} ",
log.timestamp.hour(),
log.timestamp.minute(),
log.timestamp.second()
))
.dark_gray(),
);
formatted_line.push_span(
Span::raw(format!(
"[{src}] {padding}",
src = log.source,
padding =
" ".repeat(3usize.saturating_sub(log.source.to_string().len()))
))
.style(match log.source {
TraceSrc::App(_platform) => Style::new().blue(),
TraceSrc::Dev => Style::new().magenta(),
TraceSrc::Build => Style::new().yellow(),
TraceSrc::Bundle => Style::new().magenta(),
TraceSrc::Cargo => Style::new().yellow(),
TraceSrc::Unknown => Style::new().gray(),
}),
);
formatted_line.push_span(
Span::raw(format!(
"{:02}:{:02}:{:02} ",
log.timestamp.hour(),
log.timestamp.minute(),
log.timestamp.second()
))
.dark_gray(),
);
formatted_line.push_span(
Span::raw(format!(
"[{src}] {padding}",
src = log.source,
padding =
" ".repeat(3usize.saturating_sub(log.source.to_string().len()))
))
.style(match log.source {
TraceSrc::App(_platform) => Style::new().blue(),
TraceSrc::Dev => Style::new().magenta(),
TraceSrc::Build => Style::new().yellow(),
TraceSrc::Bundle => Style::new().magenta(),
TraceSrc::Cargo => Style::new().yellow(),
TraceSrc::Unknown => Style::new().gray(),
}),
);
for span in line.spans {
formatted_line.push_span(span);
for span in line.spans {
formatted_line.push_span(span);
}
formatted_line
} else {
line
};
if is_pretending_to_be_frame {
out_line = out_line.dark_gray();
}
formatted_line
} else {
line
};
if is_pretending_to_be_frame {
out_line = out_line.dark_gray();
text.lines.push(out_line);
}
text.lines.push(out_line);
}
}
text
});
text
});
// We want to get the escaped ansii string and then by dumping the paragraph as ascii codes (again)
//
// This is important because the line_count method on paragraph takes into account the width of these codes
// the 3000 clip width is to bound log lines to a reasonable memory usage
// We could consider reusing this buffer since it's a lot to allocate, but log printing is not the
// slowest thing in the world and allocating is pretty fast...
//
// After we've dumped the ascii out, we want to call "trim_end" which ensures we don't attempt
// to print extra characters as lines, since AnsiStringBuffer will in fact attempt to print empty
// cells as characters. That might not actually be important, but we want to shrink the buffer
// before printing it
let line_count = paragraph.line_count(term_width);
AnsiStringBuffer::new(3000, line_count as u16).render(&paragraph)
// We want to get the escaped ansii string and then by dumping the paragraph as ascii codes (again)
//
// This is important because the line_count method on paragraph takes into account the width of these codes
// the 3000 clip width is to bound log lines to a reasonable memory usage
// We could consider reusing this buffer since it's a lot to allocate, but log printing is not the
// slowest thing in the world and allocating is pretty fast...
//
// After we've dumped the ascii out, we want to call "trim_end" which ensures we don't attempt
// to print extra characters as lines, since AnsiStringBuffer will in fact attempt to print empty
// cells as characters. That might not actually be important, but we want to shrink the buffer
// before printing it
let line_count = paragraph.line_count(term_width);
AnsiStringBuffer::new(3000, line_count as u16).render(&paragraph)
}
}
+14 -3
View File
@@ -111,6 +111,16 @@ impl AppRunner {
// todo(jon): we should allow rebinding to the same port in fullstack itself
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
// Add some cute logging
if self.builds_opened == 0 {
tracing::info!(
"Build completed successfully in {:?}ms, launching app! 💫",
app.app.time_taken.as_millis()
);
} else {
tracing::info!("Build completed in {:?}ms", app.app.time_taken.as_millis());
}
// Start the new app before we kill the old one to give it a little bit of time
let mut handle = AppHandle::new(app).await?;
handle
@@ -120,6 +130,7 @@ impl AppRunner {
self.builds_opened == 0 && should_open_web,
)
.await?;
self.builds_opened += 1;
self.running.insert(platform, handle);
@@ -264,17 +275,17 @@ impl AppRunner {
.arg("simctl")
.arg("get_app_container")
.arg("booted")
.arg("com.dioxuslabs")
.arg(runner.app.bundle_identifier())
.output()
.await;
if let Ok(res) = res {
tracing::debug!("Using runtime asset dir: {:?}", res);
tracing::trace!("Using runtime asset dir: {:?}", res);
if let Ok(out) = String::from_utf8(res.stdout) {
let out = out.trim();
tracing::debug!("Setting Runtime asset dir: {out:?}");
tracing::trace!("Setting Runtime asset dir: {out:?}");
runner.runtime_asst_dir = Some(PathBuf::from(out));
}
}
+15 -7
View File
@@ -204,7 +204,11 @@ impl WebServer {
}
/// Sends an updated build status to all clients.
pub(crate) async fn new_build_update(&mut self, update: &BuildUpdate) {
pub(crate) async fn new_build_update(
&mut self,
update: &BuildUpdate,
builder: &super::Builder,
) {
match update {
BuildUpdate::Progress { stage } => {
// Todo(miles): wire up more messages into the splash screen UI
@@ -220,11 +224,13 @@ impl WebServer {
krate,
..
} => {
self.build_status.set(Status::Building {
progress: (*current as f64 / *total as f64).clamp(0.0, 1.0),
build_message: format!("{krate} compiling"),
});
self.send_build_status().await;
if !builder.is_finished() {
self.build_status.set(Status::Building {
progress: (*current as f64 / *total as f64).clamp(0.0, 1.0),
build_message: format!("{krate} compiling"),
});
self.send_build_status().await;
}
}
BuildStage::OptimizingWasm {} => {}
BuildStage::Aborted => {}
@@ -250,7 +256,7 @@ impl WebServer {
return;
}
tracing::debug!("Sending hotreload to clients {:?}", reload);
tracing::trace!("Sending hotreload to clients {:?}", reload);
let msg = DevserverMsg::HotReload(reload);
let msg = serde_json::to_string(&msg).unwrap();
@@ -281,6 +287,8 @@ impl WebServer {
/// Tells all clients to reload if possible for new changes.
pub(crate) async fn send_reload_command(&mut self) {
tracing::trace!("Sending reload to toast");
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
self.build_status.set(Status::Ready);
+1
View File
@@ -0,0 +1 @@