final 0.7 fixes: xcode 15/16 hybrid, auto openssldir for android, include_*! tracking hotpatch, fix /assets/ brick at high opt levels, windows linker file, etc (#4376)

* wip: openssl autodetection

* add voltaile read inside of asset fn

* support xcode 15 for --device

* vendor openssl on android

* Smaller width TUI

* switch to anyhow for the entire CLI

* add backtrace and print double printing of cargo_metadata errors

* add some padding to multi-line anyhow error

* check semvers

* don't open if not interactive

* set the tailwind current_dir so it doesn't twitch

* get the oboe demo working

* copy the c++ library on android

* cleanup env vars

* add depinfo watcher

* fix stale hotpatches and crate count

* add musl support

* remove outdated autofmt test

* update scanner app

* add hotdog app and bundle to ci

* upload dx on every branch

* collapse mobile into desktop

* `dx doctor` command

* windows command files

* no pipe on doctor

* fix ci?

* fix links

* fix ci

* use main instead of nightly

* bump node

* build cli for host

* cargo doc

* prep dog_app for bundle

* package hotdog

* no bundle or nightly cli

* comment out nightly cli

* add quotes to linker files
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-07-14 12:54:15 -07:00
1 parent 97947b169b
commit 727752a965
78 files changed
+2270 -817

No files matched your search

-2
View File
@@ -149,9 +149,7 @@ built = { version = "0.7.5", features = ["git2"] }
[features]
default = []
plugin = []
tokio-console = ["dep:console-subscriber", "tokio/tracing"]
bundle = []
no-downloads = []
[[bin]]
+3 -4
View File
@@ -34,7 +34,7 @@ use std::{
};
use crate::Result;
use anyhow::Context;
use anyhow::{bail, Context};
use const_serialize::{ConstVec, SerializeConst};
use dioxus_cli_opt::AssetManifest;
use manganis::BundledAsset;
@@ -352,11 +352,10 @@ pub(crate) fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<AssetMa
.output()?;
if !output.status.success() {
return Err(anyhow::anyhow!(
bail!(
"Failed to re-sign the binary with codesign after finalizing the assets: {}",
String::from_utf8_lossy(&output.stderr)
)
.into());
);
}
}
+67 -29
View File
@@ -2,7 +2,7 @@ use crate::{
serve::WebServer, BuildArtifacts, BuildRequest, BuildStage, BuilderUpdate, Platform,
ProgressRx, ProgressTx, Result, StructuredOutput,
};
use anyhow::Context;
use anyhow::{bail, Context};
use dioxus_cli_opt::process_file_to;
use futures_util::{future::OptionFuture, pin_mut, FutureExt};
use itertools::Itertools;
@@ -122,25 +122,13 @@ impl AppBuilder {
/// updates (e.g., `wait`, `finish_build`).
/// - The build process is designed to be cancellable and restartable using methods like `abort_all`
/// or `rebuild`.
pub(crate) fn start(request: &BuildRequest, mode: BuildMode) -> Result<Self> {
pub(crate) fn new(request: &BuildRequest) -> Result<Self> {
let (tx, rx) = futures_channel::mpsc::unbounded();
Ok(Self {
build: request.clone(),
stage: BuildStage::Initializing,
build_task: tokio::spawn({
let request = request.clone();
let tx = tx.clone();
async move {
let ctx = BuildContext {
mode,
tx: tx.clone(),
};
request.verify_tooling(&ctx).await?;
request.prepare_build_dir()?;
request.build(&ctx).await
}
}),
build_task: tokio::task::spawn(std::future::pending()),
tx,
rx,
patches: vec![],
@@ -164,6 +152,29 @@ impl AppBuilder {
})
}
/// Create a new `AppBuilder` and immediately start a build process.
pub fn started(request: &BuildRequest, mode: BuildMode) -> Result<Self> {
let mut builder = Self::new(request)?;
builder.start(mode);
Ok(builder)
}
pub(crate) fn start(&mut self, mode: BuildMode) {
self.build_task = tokio::spawn({
let request = self.build.clone();
let tx = self.tx.clone();
async move {
let ctx = BuildContext {
mode,
tx: tx.clone(),
};
request.verify_tooling(&ctx).await?;
request.prepare_build_dir()?;
request.build(&ctx).await
}
});
}
/// Wait for any new updates to the builder - either it completed or gave us a message etc
pub(crate) async fn wait(&mut self) -> BuilderUpdate {
use futures_util::StreamExt;
@@ -178,7 +189,7 @@ impl AppBuilder {
match bundle {
Ok(Ok(bundle)) => BuilderUpdate::BuildReady { bundle },
Ok(Err(err)) => BuilderUpdate::BuildFailed { err },
Err(err) => BuilderUpdate::BuildFailed { err: crate::Error::Runtime(format!("Build panicked! {:#?}", err)) },
Err(err) => BuilderUpdate::BuildFailed { err: anyhow::anyhow!("Build panicked! {:#?}", err) },
}
},
Some(Ok(Some(msg))) = OptionFuture::from(self.stdout.as_mut().map(|f| f.next_line())) => {
@@ -596,8 +607,12 @@ impl AppBuilder {
let triple = self.build.triple.clone();
let asset_dir = self.build.asset_dir();
// Hotpatch asset!() calls
for bundled in res.assets.assets() {
let original_artifacts = self.artifacts.as_mut().unwrap();
let original_artifacts = self
.artifacts
.as_mut()
.context("No artifacts to hotpatch")?;
if original_artifacts.assets.contains(bundled) {
continue;
@@ -623,6 +638,18 @@ impl AppBuilder {
}
}
// Make sure to add `include!()` calls to the watcher so we can watch changes as they evolve
for file in res.depinfo.files.iter() {
let original_artifacts = self
.artifacts
.as_mut()
.context("No artifacts to hotpatch")?;
if !original_artifacts.depinfo.files.contains(file) {
original_artifacts.depinfo.files.push(file.clone());
}
}
tracing::debug!("Patching {} -> {}", original.display(), new.display());
let mut jump_table = crate::build::create_jump_table(&new, &triple, cache)?;
@@ -891,7 +918,7 @@ impl AppBuilder {
.context("Failed to parse xcrun output")?;
let device_uuid = json["result"]["devices"][0]["identifier"]
.as_str()
.ok_or("Failed to extract device UUID")?
.context("Failed to extract device UUID")?
.to_string();
Ok(device_uuid)
@@ -918,14 +945,14 @@ impl AppBuilder {
.await?;
if !output.status.success() {
return Err(format!("Failed to install app: {:?}", output).into());
bail!("Failed to install app: {:?}", output);
}
let json: Value = serde_json::from_str(&std::fs::read_to_string(tmpfile.path())?)
.context("Failed to parse xcrun output")?;
let installation_url = json["result"]["installedApplications"][0]["installationURL"]
.as_str()
.ok_or("Failed to extract installation URL")?
.context("Failed to extract installation URL from xcrun output")?
.to_string();
Ok(installation_url)
@@ -953,7 +980,7 @@ impl AppBuilder {
.await?;
if !output.status.success() {
return Err(format!("Failed to launch app: {:?}", output).into());
bail!("Failed to launch app: {:?}", output);
}
let json: Value = serde_json::from_str(&std::fs::read_to_string(tmpfile.path())?)
@@ -961,7 +988,7 @@ impl AppBuilder {
let status_pid = json["result"]["process"]["processIdentifier"]
.as_u64()
.ok_or("Failed to extract process identifier")?;
.context("Failed to extract process identifier")?;
let output = Command::new("xcrun")
.args([
@@ -978,7 +1005,7 @@ impl AppBuilder {
.await?;
if !output.status.success() {
return Err(format!("Failed to resume app: {:?}", output).into());
bail!("Failed to resume app: {:?}", output);
}
Ok(())
@@ -998,18 +1025,27 @@ To accept the agreement, go to https://developer.apple.com/account
To create a provisioning profile, follow the instructions here:
https://developer.apple.com/documentation/xcode/sharing-your-teams-signing-certificates"#;
let profiles_folder = dirs::home_dir()
// Check the xcode 16 location first
let mut profiles_folder = dirs::home_dir()
.context("Your machine has no home-dir")?
.join("Library/Developer/Xcode/UserData/Provisioning Profiles");
// If it doesn't exist, check the old location
if !profiles_folder.exists() {
profiles_folder = dirs::home_dir()
.context("Your machine has no home-dir")?
.join("Library/MobileDevice/Provisioning Profiles");
}
if !profiles_folder.exists() || profiles_folder.read_dir()?.next().is_none() {
tracing::error!(
r#"No provisioning profiles found when trying to codesign the app.
We checked the folder: {}
We checked the folders:
- XCode16: ~/Library/Developer/Xcode/UserData/Provisioning Profiles
- XCode15: ~/Library/MobileDevice/Provisioning Profiles
{CODESIGN_ERROR}
"#,
profiles_folder.display()
"#
)
}
@@ -1125,8 +1161,10 @@ We checked the folder: {}
.context("Failed to codesign the app")?;
if !output.status.success() {
let stderr = String::from_utf8(output.stderr).unwrap_or_default();
return Err(format!("Failed to codesign the app: {stderr}").into());
bail!(
"Failed to codesign the app: {}",
String::from_utf8(output.stderr).unwrap_or_default()
);
}
Ok(())
+1 -1
View File
@@ -105,7 +105,7 @@ impl BuildContext {
}
pub(crate) fn status_build_error(&self, line: String) {
tracing::error!(dx_src = ?TraceSrc::Cargo, "{line}");
tracing::debug!(dx_src = ?TraceSrc::Cargo, "{line}");
}
pub(crate) fn status_build_message(&self, line: String) {
+356 -128
View File
@@ -320,8 +320,9 @@ use crate::{
RustcArgs, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig, Workspace,
DX_RUSTC_WRAPPER_ENV_VAR,
};
use anyhow::Context;
use anyhow::{bail, Context};
use cargo_metadata::diagnostic::Diagnostic;
use depinfo::RustcDepInfo;
use dioxus_cli_config::format_base_path_meta_element;
use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV};
use dioxus_cli_opt::{process_file_to, AssetManifest};
@@ -331,7 +332,7 @@ use manganis::AssetOptions;
use manganis_core::AssetVariant;
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::{borrow::Cow, ffi::OsString};
use std::{
collections::{BTreeMap, HashSet},
io::Write,
@@ -391,6 +392,7 @@ pub(crate) struct BuildRequest {
pub(crate) link_args_file: Arc<NamedTempFile>,
pub(crate) link_err_file: Arc<NamedTempFile>,
pub(crate) rustc_wrapper_args_file: Arc<NamedTempFile>,
pub(crate) command_file: Arc<NamedTempFile>,
pub(crate) base_path: Option<String>,
pub(crate) using_dioxus_explicitly: bool,
}
@@ -443,6 +445,7 @@ pub struct BuildArtifacts {
pub(crate) assets: AssetManifest,
pub(crate) mode: BuildMode,
pub(crate) patch_cache: Option<Arc<HotpatchModuleCache>>,
pub(crate) depinfo: RustcDepInfo,
}
impl BuildRequest {
@@ -569,13 +572,10 @@ impl BuildRequest {
},
None if !using_dioxus_explicitly => Platform::autodetect_from_cargo_feature("desktop").unwrap(),
None => match enabled_platforms.len() {
0 => return Err(anyhow::anyhow!("No platform specified and no platform marked as default in Cargo.toml. Try specifying a platform with `--platform`").into()),
0 => bail!("No platform specified and no platform marked as default in Cargo.toml. Try specifying a platform with `--platform`"),
1 => enabled_platforms[0],
_ => {
return Err(anyhow::anyhow!(
"Multiple platforms enabled in Cargo.toml. Please specify a platform with `--platform` or set a default platform in Cargo.toml"
)
.into())
bail!("Multiple platforms enabled in Cargo.toml. Please specify a platform with `--platform` or set a default platform in Cargo.toml")
}
},
};
@@ -662,7 +662,13 @@ impl BuildRequest {
"-Clink-arg=-landroid".to_string(),
"-Clink-arg=-llog".to_string(),
"-Clink-arg=-lOpenSLES".to_string(),
"-Clink-arg=-lc++".to_string(),
"-Clink-arg=-lc++abi".to_string(),
"-Clink-arg=-Wl,--export-dynamic".to_string(),
format!(
"-Clink-arg=-Wl,--sysroot={}",
workspace.android_tools()?.sysroot().display()
),
]);
}
@@ -712,6 +718,9 @@ impl BuildRequest {
let session_cache_dir = Arc::new(
TempDir::new().context("Failed to create temporary directory for session cache")?,
);
let command_file = Arc::new(
NamedTempFile::new().context("Failed to create temporary file for linker args")?,
);
let extra_rustc_args = shell_words::split(&args.rustc_args.clone().unwrap_or_default())
.context("Failed to parse rustc args")?;
@@ -752,6 +761,7 @@ impl BuildRequest {
custom_linker,
link_args_file,
link_err_file,
command_file,
session_cache_dir,
rustc_wrapper_args_file,
extra_rustc_args,
@@ -774,6 +784,9 @@ impl BuildRequest {
// We need to make sure to not react to this though, so the filemap must cache it
_ = self.bust_fingerprint(ctx);
// Run any pre-build steps like tailwind, etc
self.prebuild().await?;
// Run the cargo build to produce our artifacts
let mut artifacts = self.cargo_build(ctx).await?;
@@ -793,12 +806,23 @@ impl BuildRequest {
ctx.status_start_bundle();
self.write_executable(ctx, &artifacts.exe, &mut artifacts.assets)
.await?;
self.write_frameworks(ctx, &artifacts.direct_rustc).await?;
self.write_assets(ctx, &artifacts.assets).await?;
self.write_metadata().await?;
self.optimize(ctx).await?;
self.assemble(ctx).await?;
.await
.context("Failed to write executable")?;
self.write_frameworks(ctx, &artifacts.direct_rustc)
.await
.context("Failed to write frameworks")?;
self.write_assets(ctx, &artifacts.assets)
.await
.context("Failed to write assets")?;
self.write_metadata()
.await
.context("Failed to write metadata")?;
self.optimize(ctx)
.await
.context("Failed to optimize build")?;
self.assemble(ctx)
.await
.context("Failed to assemble build")?;
tracing::debug!("Bundle created at {}", self.root_dir().display());
}
@@ -916,10 +940,7 @@ impl BuildRequest {
// since that is a really bad user experience.
Message::BuildFinished(finished) => {
if !finished.success {
return Err(anyhow::anyhow!(
"Cargo build failed, signaled by the compiler. Toggle tracing mode (press `t`) for more information."
)
.into());
bail!("cargo build finished with errors.")
}
}
_ => {}
@@ -971,6 +992,7 @@ impl BuildRequest {
let time_end = SystemTime::now();
let mode = ctx.mode.clone();
let platform = self.platform;
let depinfo = RustcDepInfo::from_file(&exe.with_extension("d")).unwrap_or_default();
tracing::debug!(
"Build completed successfully in {}us: {:?}",
@@ -986,6 +1008,7 @@ impl BuildRequest {
time_start,
assets,
mode,
depinfo,
patch_cache: None,
})
}
@@ -1079,6 +1102,7 @@ impl BuildRequest {
// todo - how do we handle windows dlls? we don't want to bundle the system dlls
// for now, we don't do anything with dlls, and only use .dylibs and .so files
// Write dylibs and dlls to the frameworks folder
if arg.ends_with(".dylib") | arg.ends_with(".so") {
let from = PathBuf::from(arg);
let to = framework_dir.join(from.file_name().unwrap());
@@ -1104,6 +1128,16 @@ impl BuildRequest {
std::fs::copy(from, to)?;
}
}
// On android, the c++_static flag means we need to copy the libc++_shared.so precompiled
// library to the jniLibs folder
if arg.contains("-lc++_static") && self.platform == Platform::Android {
std::fs::copy(
self.workspace.android_tools()?.libcpp_shared(&self.triple),
framework_dir.join("libc++_shared.so"),
)
.with_context(|| "Failed to copy libc++_shared.so into bundle")?;
}
}
Ok(())
@@ -1115,6 +1149,13 @@ impl BuildRequest {
self.root_dir().join("Contents").join("Frameworks")
}
OperatingSystem::IOS(_) => self.root_dir().join("Frameworks"),
OperatingSystem::Linux if self.platform == Platform::Android => self
.root_dir()
.join("app")
.join("src")
.join("main")
.join("jniLibs")
.join("arm64-v8a"),
OperatingSystem::Linux | OperatingSystem::Windows => self.root_dir(),
_ => self.root_dir(),
}
@@ -1129,14 +1170,6 @@ impl BuildRequest {
return Ok(());
}
// Run the tailwind build before bundling anything else
crate::TailwindCli::run_once(
self.package_manifest_dir(),
self.config.application.tailwind_input.clone(),
self.config.application.tailwind_output.clone(),
)
.await?;
let asset_dir = self.asset_dir();
// First, clear the asset dir of any files that don't exist in the new manifest
@@ -1379,15 +1412,28 @@ impl BuildRequest {
tracing::trace!("Linking with {:?} using args: {:#?}", linker, object_files);
let mut out_args: Vec<OsString> = vec![];
out_args.extend(object_files.iter().map(Into::into));
out_args.extend(dylibs.iter().map(Into::into));
out_args.extend(self.thin_link_args(&args)?.iter().map(Into::into));
out_args.extend(out_arg.iter().map(Into::into));
if cfg!(windows) {
let cmd_contents: String = out_args
.iter()
.map(|s| format!("\"{}\"", s.to_string_lossy()))
.join(" ");
std::fs::write(self.command_file.path(), cmd_contents)
.context("Failed to write linker command file")?;
out_args = vec![format!("@{}", self.command_file.path().display()).into()];
}
// Run the linker directly!
//
// We dump its output directly into the patch exe location which is different than how rustc
// does it since it uses llvm-objcopy into the `target/debug/` folder.
let res = Command::new(linker)
.args(object_files.iter())
.args(dylibs.iter())
.args(self.thin_link_args(&args)?)
.args(out_arg)
.args(out_args)
.env_clear()
.envs(rustc_args.envs.iter().map(|(k, v)| (k, v)))
.output()
@@ -1561,7 +1607,7 @@ impl BuildRequest {
}
LinkerFlavor::Unsupported => {
return Err(anyhow::anyhow!("Unsupported platform for thin linking").into())
bail!("Unsupported platform for thin linking")
}
}
@@ -1791,7 +1837,7 @@ impl BuildRequest {
// And then remove the rest of the rlibs
//
// We also need to insert the -force_load flag to force the linker to load the archive
let mut args = rustc_args.link_args.clone();
let mut args: Vec<_> = rustc_args.link_args.iter().skip(1).cloned().collect();
if let Some(last_object) = args.iter().rposition(|arg| arg.ends_with(".o")) {
if archive_has_contents {
match self.linker_flavor() {
@@ -1879,21 +1925,33 @@ impl BuildRequest {
args.remove(flavor_idx);
}
// Set the output file
match self.triple.operating_system {
OperatingSystem::Windows => args.push(format!("/OUT:{}", exe.display())),
_ => args.extend(["-o".to_string(), exe.display().to_string()]),
}
// And now we can run the linker with our new args
let linker = self.select_linker()?;
tracing::trace!("Fat linking with args: {:?} {:#?}", linker, args);
tracing::trace!("Fat linking with env: {:#?}", rustc_args.envs);
tracing::trace!("Fat linking with env:");
for e in rustc_args.envs.iter() {
tracing::trace!(" {}={}", e.0, e.1);
}
// Handle windows command files
let mut out_args = args.clone();
if cfg!(windows) {
let cmd_contents: String = out_args.iter().map(|f| format!("\"{f}\"")).join(" ");
std::fs::write(self.command_file.path(), cmd_contents)
.context("Failed to write linker command file")?;
out_args = vec![format!("@{}", self.command_file.path().display())];
}
// Run the linker directly!
let out_arg = match self.triple.operating_system {
OperatingSystem::Windows => vec![format!("/OUT:{}", exe.display())],
_ => vec!["-o".to_string(), exe.display().to_string()],
};
let res = Command::new(linker)
.args(args.iter().skip(1))
.args(out_arg)
.args(out_args)
.env_clear()
.envs(rustc_args.envs.iter().map(|(k, v)| (k, v)))
.output()
@@ -2062,12 +2120,8 @@ impl BuildRequest {
cmd.arg("-Crelocation-model=pic");
}
tracing::debug!("Direct rustc: {:#?}", cmd);
cmd.envs(rustc_args.envs.iter().cloned());
// tracing::trace!("Setting env vars: {:#?}", rustc_args.envs);
Ok(cmd)
}
@@ -2084,16 +2138,24 @@ impl BuildRequest {
_ => {
let mut cmd = Command::new("cargo");
let env = self.cargo_build_env_vars(ctx)?;
let args = self.cargo_build_arguments(ctx);
tracing::trace!("Building with cargo rustc");
for e in env.iter() {
tracing::trace!(": {}={}", e.0, e.1);
}
for a in args.iter() {
tracing::trace!(": {}", a);
}
cmd.arg("rustc")
.current_dir(self.crate_dir())
.arg("--message-format")
.arg("json-diagnostic-rendered-ansi")
.args(self.cargo_build_arguments(ctx))
.envs(
self.cargo_build_env_vars(ctx)?
.iter()
.map(|(k, v)| (k.as_ref(), v)),
);
.args(args)
.envs(env.iter().map(|(k, v)| (k.as_ref(), v)));
if ctx.mode == BuildMode::Fat {
cmd.env(
@@ -2109,8 +2171,6 @@ impl BuildRequest {
);
}
tracing::debug!("Cargo: {:#?}", cmd);
Ok(cmd)
}
}
@@ -2351,7 +2411,67 @@ impl BuildRequest {
Ok(env_vars)
}
/// Set the environment variables required for building on Android.
///
/// This involves setting sysroots, CC, CXX, AR, and other environment variables along with
/// vars that cc-rs uses for its C/C++ compilation.
///
/// We pulled the environment setup from `cargo ndk` and attempt to mimic its behavior to retain
/// compatibility with existing crates that work with `cargo ndk`.
///
/// <https://github.com/bbqsrc/cargo-ndk/blob/1d1a6dc70a99b7f95bc71ed07bf893ef37966efc/src/cargo.rs#L97-L102>
///
/// cargo-ndk is MIT licensed.
///
/// <https://github.com/bbqsrc/cargo-ndk>
fn android_env_vars(&self) -> Result<Vec<(Cow<'static, str>, String)>> {
// Derived from getenv_with_target_prefixes in `cc` crate.
fn cc_env(var_base: &str, triple: &str) -> (String, Option<String>) {
#[inline]
fn env_var_with_key(key: String) -> Option<(String, String)> {
std::env::var(&key).map(|value| (key, value)).ok()
}
let triple_u = triple.replace('-', "_");
let most_specific_key = format!("{}_{}", var_base, triple);
env_var_with_key(most_specific_key.to_string())
.or_else(|| env_var_with_key(format!("{}_{}", var_base, triple_u)))
.or_else(|| env_var_with_key(format!("TARGET_{}", var_base)))
.or_else(|| env_var_with_key(var_base.to_string()))
.map(|(key, value)| (key, Some(value)))
.unwrap_or_else(|| (most_specific_key, None))
}
fn cargo_env_target_cfg(triple: &str, key: &str) -> String {
format!("CARGO_TARGET_{}_{}", &triple.replace('-', "_"), key).to_uppercase()
}
fn clang_target(rust_target: &str, api_level: u8) -> String {
let target = match rust_target {
"arm-linux-androideabi" => "armv7a-linux-androideabi",
"armv7-linux-androideabi" => "armv7a-linux-androideabi",
_ => rust_target,
};
format!("--target={target}{api_level}")
}
fn sysroot_target(rust_target: &str) -> &str {
(match rust_target {
"armv7-linux-androideabi" => "arm-linux-androideabi",
_ => rust_target,
}) as _
}
fn rt_builtins(rust_target: &str) -> &str {
(match rust_target {
"armv7-linux-androideabi" => "arm",
"aarch64-linux-android" => "aarch64",
"i686-linux-android" => "i686",
"x86_64-linux-android" => "x86_64",
_ => rust_target,
}) as _
}
let mut env_vars: Vec<(Cow<'static, str>, String)> = vec![];
let tools = self.workspace.android_tools()?;
@@ -2361,7 +2481,7 @@ impl BuildRequest {
let target_cc = tools.target_cc();
let target_cxx = tools.target_cxx();
let java_home = tools.java_home();
let ndk = tools.ndk.clone();
let ndk_home = tools.ndk.clone();
tracing::debug!(
r#"Using android:
min_sdk_version: {min_sdk_version}
@@ -2372,61 +2492,153 @@ impl BuildRequest {
java_home: {java_home:?}
"#
);
env_vars.push((
"ANDROID_NATIVE_API_LEVEL".into(),
min_sdk_version.to_string(),
));
env_vars.push(("TARGET_AR".into(), ar_path.display().to_string()));
env_vars.push(("TARGET_CC".into(), target_cc.display().to_string()));
env_vars.push(("TARGET_CXX".into(), target_cxx.display().to_string()));
env_vars.push((
format!(
"CARGO_TARGET_{}_LINKER",
self.triple
.to_string()
.to_ascii_uppercase()
.replace("-", "_")
)
.into(),
linker.display().to_string(),
));
env_vars.push(("ANDROID_NDK_ROOT".into(), ndk.display().to_string()));
if let Some(java_home) = java_home {
tracing::debug!("Setting JAVA_HOME to {java_home:?}");
env_vars.push(("JAVA_HOME".into(), java_home.display().to_string()));
}
// Set the wry env vars - this is where wry will dump its kotlin files.
// Their setup is really annyoing and requires us to hardcode `dx` to specific versions of tao/wry.
env_vars.push(("WRY_ANDROID_PACKAGE".into(), "dev.dioxus.main".to_string()));
env_vars.push(("WRY_ANDROID_LIBRARY".into(), "dioxusmain".to_string()));
env_vars.push((
"WRY_ANDROID_KOTLIN_FILES_OUT_DIR".into(),
self.wry_android_kotlin_files_out_dir()
.display()
.to_string(),
));
let triple = self.triple.to_string();
// todo(jon): the guide for openssl recommends extending the path to include the tools dir
// in practice I couldn't get this to work, but this might eventually become useful.
//
// https://github.com/openssl/openssl/blob/master/NOTES-ANDROID.md#configuration
//
// They recommend a configuration like this:
//
// // export ANDROID_NDK_ROOT=/home/whoever/Android/android-sdk/ndk/20.0.5594570
// PATH=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin:$ANDROID_NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin:$PATH
// ./Configure android-arm64 -D__ANDROID_API__=29
// make
//
// let tools_dir = arch.android_tools_dir(&ndk);
// let extended_path = format!(
// "{}:{}",
// tools_dir.display(),
// std::env::var("PATH").unwrap_or_default()
// );
// env_vars.push(("PATH", extended_path));
// Environment variables for the `cc` crate
let (cc_key, _cc_value) = cc_env("CC", &triple);
let (cflags_key, cflags_value) = cc_env("CFLAGS", &triple);
let (cxx_key, _cxx_value) = cc_env("CXX", &triple);
let (cxxflags_key, cxxflags_value) = cc_env("CXXFLAGS", &triple);
let (ar_key, _ar_value) = cc_env("AR", &triple);
let (ranlib_key, _ranlib_value) = cc_env("RANLIB", &triple);
// Environment variables for cargo
let cargo_ar_key = cargo_env_target_cfg(&triple, "ar");
let cargo_rust_flags_key = cargo_env_target_cfg(&triple, "rustflags");
let bindgen_clang_args_key =
format!("BINDGEN_EXTRA_CLANG_ARGS_{}", &triple.replace('-', "_"));
let clang_target = clang_target(&self.triple.to_string(), min_sdk_version as _);
let target_cc = tools.target_cc();
let target_cflags = match cflags_value {
Some(v) => format!("{clang_target} {v}"),
None => clang_target.to_string(),
};
let target_cxx = tools.target_cxx();
let target_cxxflags = match cxxflags_value {
Some(v) => format!("{clang_target} {v}"),
None => clang_target.to_string(),
};
let cargo_ndk_sysroot_path_key = "CARGO_NDK_SYSROOT_PATH";
let cargo_ndk_sysroot_path = tools.sysroot();
let cargo_ndk_sysroot_target_key = "CARGO_NDK_SYSROOT_TARGET";
let cargo_ndk_sysroot_target = sysroot_target(&triple);
let cargo_ndk_sysroot_libs_path_key = "CARGO_NDK_SYSROOT_LIBS_PATH";
let cargo_ndk_sysroot_libs_path = cargo_ndk_sysroot_path
.join("usr")
.join("lib")
.join(cargo_ndk_sysroot_target);
let target_ar = tools.ar_path();
let target_ranlib = tools.ranlib();
let clang_folder = tools.clang_folder();
// choose the clang target with the highest version
// Should we filter for only numbers?
let clang_builtins_target = std::fs::read_dir(clang_folder)
.expect("Unable to get clang target directory")
.filter_map(|a| a.ok())
.max_by(|a, b| a.file_name().cmp(&b.file_name()))
.expect("Unable to get clang target")
.path();
let clang_rt = format!(
"-L{} -lstatic=clang_rt.builtins-{}-android",
clang_builtins_target.join("lib").join("linux").display(),
rt_builtins(&triple)
);
let extra_include: String = format!(
"{}/usr/include/{}",
&cargo_ndk_sysroot_path.display(),
&cargo_ndk_sysroot_target
);
let bindgen_args = format!(
"--sysroot={} -I{}",
&cargo_ndk_sysroot_path.display(),
extra_include
);
for env in [
(cc_key, target_cc.clone().into_os_string()),
(cflags_key, target_cflags.into()),
(cxx_key, target_cxx.into_os_string()),
(cxxflags_key, target_cxxflags.into()),
(ar_key, target_ar.clone().into()),
(ranlib_key, target_ranlib.into_os_string()),
(cargo_ar_key, target_ar.into_os_string()),
(
cargo_ndk_sysroot_path_key.to_string(),
cargo_ndk_sysroot_path.clone().into_os_string(),
),
(
cargo_ndk_sysroot_libs_path_key.to_string(),
cargo_ndk_sysroot_libs_path.into_os_string(),
),
(
cargo_ndk_sysroot_target_key.to_string(),
cargo_ndk_sysroot_target.into(),
),
(cargo_rust_flags_key, clang_rt.into()),
(bindgen_clang_args_key, bindgen_args.into()),
(
"ANDROID_NATIVE_API_LEVEL".to_string(),
min_sdk_version.to_string().into(),
),
(
format!(
"CARGO_TARGET_{}_LINKER",
self.triple
.to_string()
.to_ascii_uppercase()
.replace("-", "_")
),
linker.into_os_string(),
),
("ANDROID_NDK_ROOT".to_string(), ndk_home.into_os_string()),
// Set the wry env vars - this is where wry will dump its kotlin files.
// Their setup is really annyoing and requires us to hardcode `dx` to specific versions of tao/wry.
(
"WRY_ANDROID_PACKAGE".to_string(),
"dev.dioxus.main".to_string().into(),
),
(
"WRY_ANDROID_LIBRARY".to_string(),
"dioxusmain".to_string().into(),
),
(
"WRY_ANDROID_KOTLIN_FILES_OUT_DIR".to_string(),
self.wry_android_kotlin_files_out_dir().into_os_string(),
),
// Found this through a comment related to bindgen using the wrong clang for cross compiles
//
// https://github.com/rust-lang/rust-bindgen/issues/2962#issuecomment-2438297124
//
// https://github.com/KyleMayes/clang-sys?tab=readme-ov-file#environment-variables
("CLANG_PATH".into(), target_cc.with_extension("exe").into()),
] {
env_vars.push((
env.0.into(),
env.1
.to_str()
.expect("Failed to convert env var value to string")
.to_string(),
));
}
if std::env::var("MSYSTEM").is_ok() || std::env::var("CYGWIN").is_ok() {
for var in env_vars.iter_mut() {
// Convert windows paths to unix-style paths
// This is a workaround for the fact that the `cc` crate expects unix-style paths
// and will fail if it encounters windows-style paths.
var.1 = var.1.replace('\\', "/");
}
}
Ok(env_vars)
}
@@ -2465,7 +2677,7 @@ impl BuildRequest {
let output = tokio::process::Command::new("cargo")
.arg("+nightly")
.arg("build")
.arg("rustc")
.arg("--unit-graph")
.arg("-Z")
.arg("unstable-options")
@@ -2479,7 +2691,11 @@ impl BuildRequest {
.await?;
if !output.status.success() {
return Err(anyhow::anyhow!("Failed to get unit count").into());
tracing::trace!(
"Failed to get unit count: {}",
String::from_utf8_lossy(&output.stderr)
);
bail!("Failed to get unit count");
}
let output_text = String::from_utf8(output.stdout).context("Failed to get unit count")?;
@@ -2911,15 +3127,6 @@ impl BuildRequest {
})
}
/// Return the version of the wasm-bindgen crate if it exists
fn wasm_bindgen_version(&self) -> Option<String> {
self.workspace
.krates
.krates_by_name("wasm-bindgen")
.next()
.map(|krate| krate.krate.version.to_string())
}
/// Return the platforms that are enabled for the package
///
/// Ideally only one platform is enabled but we need to be able to
@@ -3224,6 +3431,7 @@ impl BuildRequest {
let post_bindgen_wasm = self.wasm_bindgen_wasm_output_file();
let should_bundle_split: bool = self.wasm_split;
let bindgen_version = self
.workspace
.wasm_bindgen_version()
.expect("this should have been checked by tool verification");
@@ -3291,10 +3499,7 @@ impl BuildRequest {
ctx.status_splitting_bundle();
if !will_wasm_opt {
return Err(anyhow::anyhow!(
"Bundle splitting should automatically enable wasm-opt, but it was not enabled."
)
.into());
bail!("Bundle splitting should automatically enable wasm-opt, but it was not enabled.");
}
// Load the contents of these binaries since we need both of them
@@ -3549,7 +3754,7 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
},
)
.map_err(|e| e.into()),
_ => Err(anyhow::anyhow!("Unsupported platform for Info.plist").into()),
_ => Err(anyhow::anyhow!("Unsupported platform for Info.plist")),
}
}
@@ -3574,7 +3779,10 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
.await?;
if !output.status.success() {
return Err(anyhow::anyhow!("Failed to assemble apk: {output:?}").into());
bail!(
"Failed to assemble apk: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
@@ -3593,7 +3801,10 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
.context("Failed to run gradle bundleRelease")?;
if !output.status.success() {
return Err(anyhow::anyhow!("Failed to bundleRelease: {output:?}").into());
bail!(
"Failed to bundleRelease: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let app_release = self
@@ -3688,7 +3899,7 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
});
if let Err(e) = success.as_ref() {
return Err(format!("Failed to initialize build directory: {e}").into());
bail!("Failed to initialize build directory: {e}");
}
Ok(())
@@ -3816,15 +4027,16 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
// Ensure target is installed.
if !self.workspace.has_wasm32_unknown_unknown() {
return Err(Error::Other(anyhow::anyhow!(
"Missing target wasm32-unknown-unknown."
)));
bail!("Missing target wasm32-unknown-unknown.");
}
// Wasm bindgen
let krate_bindgen_version = self.wasm_bindgen_version().ok_or(anyhow::anyhow!(
"failed to detect wasm-bindgen version, unable to proceed"
))?;
let krate_bindgen_version =
self.workspace
.wasm_bindgen_version()
.ok_or(anyhow::anyhow!(
"failed to detect wasm-bindgen version, unable to proceed"
))?;
WasmBindgen::verify_install(&krate_bindgen_version).await?;
@@ -3884,9 +4096,9 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
return Ok(());
}
Err(anyhow::anyhow!(
"Android linker not found. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation."
).into())
bail!(
"Android linker not found at {linker:?}. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation."
);
}
/// Ensure the right dependencies are installed for linux apps.
@@ -4347,4 +4559,20 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
.flat_map(|arg| ["--config".to_string(), arg])
.collect()
}
async fn prebuild(&self) -> Result<()> {
if self.platform == Platform::Server {
return Ok(());
}
// Run the tailwind build before bundling anything else
crate::TailwindCli::run_once(
self.package_manifest_dir(),
self.config.application.tailwind_input.clone(),
self.config.application.tailwind_output.clone(),
)
.await?;
Ok(())
}
}
+40
View File
@@ -170,6 +170,13 @@ impl AndroidTools {
))
}
pub(crate) fn sysroot(&self) -> PathBuf {
// The sysroot is usually located in the NDK under:
// "~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/sysroot"
// or similar, depending on the platform.
self.android_tools_dir().parent().unwrap().join("sysroot")
}
pub(crate) fn sdk(&self) -> PathBuf {
// /Users/jonathankelley/Library/Android/sdk/ndk/25.2/... (25.2 is the ndk here)
// /Users/jonathankelley/Library/Android/sdk/
@@ -187,6 +194,21 @@ impl AndroidTools {
24
}
pub(crate) fn clang_folder(&self) -> PathBuf {
// The clang folder is usually located in the NDK under:
// "~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/lib/clang/<version>"
// or similar, depending on the platform.
self.android_tools_dir()
.parent()
.unwrap()
.join("lib")
.join("clang")
}
pub(crate) fn ranlib(&self) -> PathBuf {
self.android_tools_dir().join("llvm-ranlib")
}
pub(crate) fn ar_path(&self) -> PathBuf {
self.android_tools_dir().join("llvm-ar")
}
@@ -261,6 +283,24 @@ impl AndroidTools {
triple
}
pub(crate) fn libcpp_shared(&self, triple: &Triple) -> PathBuf {
// The libc++_shared.so is usually located in the sysroot under:
// "~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/<arch>/libc++_shared.so"
// or similar, depending on the platform.
self.sysroot()
.join("usr")
.join("lib")
.join(Self::sysroot_target(&triple.to_string()))
.join("libc++_shared.so")
}
pub(crate) fn sysroot_target(rust_target: &str) -> &str {
(match rust_target {
"armv7-linux-androideabi" => "arm-linux-androideabi",
_ => rust_target,
}) as _
}
}
fn var_or_debug(name: &str) -> Option<PathBuf> {
+22 -40
View File
@@ -1,6 +1,6 @@
use super::{check::collect_rs_files, *};
use crate::Workspace;
use anyhow::Context;
use anyhow::{bail, Context};
use dioxus_autofmt::{IndentOptions, IndentType};
use rayon::prelude::*;
use std::{borrow::Cow, fs, path::Path};
@@ -54,11 +54,9 @@ impl Autoformat {
} else if let Some(raw) = raw {
// Format raw text.
let indent = indentation_for(".", self.split_line_attributes)?;
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
println!("{}", inner);
} else {
return Err("error formatting codeblock".into());
}
let formatted =
dioxus_autofmt::fmt_block(&raw, 0, indent).context("error formatting codeblock")?;
println!("{}", formatted);
} else {
// Default to formatting the project.
let crate_dir = if let Some(package) = self.package {
@@ -76,11 +74,8 @@ impl Autoformat {
Cow::Borrowed(Path::new("."))
};
if let Err(e) =
autoformat_project(check, split_line_attributes, format_rust_code, crate_dir)
{
return Err(format!("error formatting project: {}", e).into());
}
autoformat_project(check, split_line_attributes, format_rust_code, crate_dir)
.context("error autoformatting project")?;
}
Ok(StructuredOutput::Success)
@@ -100,19 +95,15 @@ fn refactor_file(
} else {
fs::read_to_string(&file)
};
let Ok(mut s) = file_content else {
return Err(format!("failed to open file: {}", file_content.unwrap_err()).into());
};
let mut s = file_content.context("failed to open file")?;
if format_rust_code {
s = format_rust(&s)?;
}
let Ok(Ok(edits)) =
syn::parse_file(&s).map(|file| dioxus_autofmt::try_fmt_file(&s, &file, indent))
else {
return Err(format!("failed to format file: {}", s).into());
};
let parsed = syn::parse_file(&s).context("failed to parse file")?;
let edits =
dioxus_autofmt::try_fmt_file(&s, &parsed, indent).context("failed to format file")?;
let out = dioxus_autofmt::apply_formats(&s, edits);
@@ -135,18 +126,16 @@ fn format_file(
let mut contents = fs::read_to_string(&path)?;
let mut if_write = false;
if format_rust_code {
let formatted = format_rust(&contents)
.map_err(|err| Error::Parse(format!("Syntax Error:\n{}", err)))?;
let formatted = format_rust(&contents).context("Syntax Error")?;
if contents != formatted {
if_write = true;
contents = formatted;
}
}
let parsed = syn::parse_file(&contents)
.map_err(|err| Error::Parse(format!("Failed to parse file: {}", err)))?;
let parsed = syn::parse_file(&contents).context("Failed to parse file")?;
let edits = dioxus_autofmt::try_fmt_file(&contents, &parsed, indent)
.map_err(|err| Error::Parse(format!("Failed to format file: {}", err)))?;
.context("Failed to format file")?;
let len = edits.len();
if !edits.is_empty() {
@@ -202,7 +191,7 @@ fn autoformat_project(
let files_formatted: usize = counts.into_iter().flatten().sum();
if files_formatted > 0 && check {
return Err(format!("{} files needed formatting", files_formatted).into());
bail!("{} files needed formatting", files_formatted);
}
Ok(())
@@ -220,9 +209,7 @@ fn indentation_for(
.output()?;
if !out.status.success() {
return Err(Error::Runtime(format!(
"cargo fmt failed with status: {out:?}"
)));
bail!("cargo fmt failed with status: {out:?}");
}
let config = String::from_utf8_lossy(&out.stdout);
@@ -232,18 +219,14 @@ fn indentation_for(
.find(|line| line.starts_with("hard_tabs "))
.and_then(|line| line.split_once('='))
.map(|(_, value)| value.trim() == "true")
.ok_or_else(|| {
Error::Runtime("Could not find hard_tabs option in rustfmt config".into())
})?;
.context("Could not find hard_tabs option in rustfmt config")?;
let tab_spaces = config
.lines()
.find(|line| line.starts_with("tab_spaces "))
.and_then(|line| line.split_once('='))
.map(|(_, value)| value.trim().parse::<usize>())
.ok_or_else(|| Error::Runtime("Could not find tab_spaces option in rustfmt config".into()))?
.map_err(|_| {
Error::Runtime("Could not parse tab_spaces option in rustfmt config".into())
})?;
.context("Could not find tab_spaces option in rustfmt config")?
.context("Could not parse tab_spaces option in rustfmt config")?;
Ok(IndentOptions::new(
if hard_tabs {
@@ -258,7 +241,9 @@ fn indentation_for(
/// Format rust code using prettyplease
fn format_rust(input: &str) -> Result<String> {
let syntax_tree = syn::parse_file(input).map_err(format_syn_error)?;
let syntax_tree = syn::parse_file(input)
.map_err(format_syn_error)
.context("Failed to parse Rust syntax")?;
let output = prettyplease::unparse(&syntax_tree);
Ok(output)
}
@@ -267,10 +252,7 @@ fn format_syn_error(err: syn::Error) -> Error {
let start = err.span().start();
let line = start.line;
let column = start.column;
Error::Parse(format!(
"Syntax Error in line {} column {}:\n{}",
line, column, err
))
anyhow::anyhow!("Syntax Error in line {} column {}:\n{}", line, column, err)
}
#[tokio::test]
+2 -2
View File
@@ -70,7 +70,7 @@ impl CommandWithPlatformOverrides<BuildArgs> {
let ssg = self.shared.ssg;
let targets = self.into_targets().await?;
AppBuilder::start(&targets.client, BuildMode::Base)?
AppBuilder::started(&targets.client, BuildMode::Base)?
.finish_build()
.await?;
@@ -78,7 +78,7 @@ impl CommandWithPlatformOverrides<BuildArgs> {
if let Some(server) = targets.server.as_ref() {
// If the server is present, we need to build it as well
let mut server_build = AppBuilder::start(server, BuildMode::Base)?;
let mut server_build = AppBuilder::started(server, BuildMode::Base)?;
server_build.finish_build().await?;
// Run SSG and cache static routes
+5 -5
View File
@@ -1,5 +1,5 @@
use crate::{AppBuilder, BuildArgs, BuildMode, BuildRequest, Platform};
use anyhow::{anyhow, Context};
use anyhow::{bail, Context};
use path_absolutize::Absolutize;
use std::collections::HashMap;
use tauri_bundler::{BundleBinary, BundleSettings, PackageSettings, SettingsBuilder};
@@ -39,7 +39,7 @@ impl Bundle {
let BuildTargets { client, server } = self.args.into_targets().await?;
AppBuilder::start(&client, BuildMode::Base)?
AppBuilder::started(&client, BuildMode::Base)?
.finish_build()
.await?;
@@ -47,7 +47,7 @@ impl Bundle {
if let Some(server) = server.as_ref() {
// If the server is present, we need to build it as well
AppBuilder::start(server, BuildMode::Base)?
AppBuilder::started(server, BuildMode::Base)?
.finish_build()
.await?;
@@ -167,10 +167,10 @@ impl Bundle {
// Check if required fields are provided instead of failing silently.
if bundle_settings.identifier.is_none() {
return Err(anyhow!("\n\nBundle identifier was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\nidentifier = \"com.mycompany\"\n\n").into());
bail!("\n\nBundle identifier was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\nidentifier = \"com.mycompany\"\n\n");
}
if bundle_settings.publisher.is_none() {
return Err(anyhow!("\n\nBundle publisher was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\npublisher = \"MyCompany\"\n\n").into());
bail!("\n\nBundle publisher was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\npublisher = \"MyCompany\"\n\n");
}
if cfg!(windows) {
+3 -3
View File
@@ -5,7 +5,7 @@
use super::*;
use crate::BuildRequest;
use anyhow::Context;
use anyhow::{anyhow, Context};
use futures_util::{stream::FuturesUnordered, StreamExt};
use std::path::Path;
@@ -118,8 +118,8 @@ async fn check_files_and_report(files_to_check: Vec<PathBuf>) -> Result<()> {
tracing::info!("No issues found.");
Ok(())
}
1 => Err("1 issue found.".into()),
_ => Err(format!("{} issues found.", total_issues).into()),
1 => Err(anyhow!("1 issue found.")),
_ => Err(anyhow!("{} issues found.", total_issues)),
}
}
-24
View File
@@ -1,24 +0,0 @@
use super::*;
use crate::Result;
/// Clean build artifacts.
///
/// Simply runs `cargo clean`
#[derive(Clone, Debug, Parser)]
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) async fn clean(self) -> Result<StructuredOutput> {
let output = tokio::process::Command::new("cargo")
.arg("clean")
.output()
.await?;
if !output.status.success() {
return Err(anyhow::anyhow!("Cargo clean failed.").into());
}
Ok(StructuredOutput::Success)
}
}
+7 -9
View File
@@ -1,5 +1,6 @@
use super::*;
use crate::TraceSrc;
use anyhow::{bail, Context};
use cargo_generate::{GenerateArgs, TemplatePath, Vcs};
use std::path::Path;
@@ -147,9 +148,9 @@ pub(crate) fn name_from_path(path: &Path) -> Result<String> {
.absolutize()?
.to_path_buf()
.file_name()
.ok_or("Current path does not include directory name".to_string())?
.context("Current path does not include directory name".to_string())?
.to_str()
.ok_or("Current directory name is not a valid UTF-8 string".to_string())?
.context("Current directory name is not a valid UTF-8 string".to_string())?
.to_string())
}
@@ -167,10 +168,7 @@ pub(crate) fn post_create(path: &Path, vcs: &Vcs) -> Result<()> {
// Only 1 error means that CWD isn't a cargo project.
Err(cargo_metadata::Error::CargoMetadata { .. }) => None,
Err(err) => {
return Err(Error::Other(anyhow::anyhow!(
"Couldn't retrieve cargo metadata: {:?}",
err
)));
anyhow::bail!("Couldn't retrieve cargo metadata: {:?}", err)
}
}
};
@@ -286,9 +284,9 @@ pub(crate) async fn connectivity_check() -> Result<()> {
}
}
Err(Error::Network(
"Error connecting to template repository. Try cloning the template manually or add `dioxus` to a `cargo new` project.".to_string(),
))
bail!(
"Error connecting to template repository. Try cloning the template manually or add `dioxus` to a `cargo new` project."
)
}
// todo: re-enable these tests with better parallelization
+212
View File
@@ -0,0 +1,212 @@
use super::*;
use crate::{Result, Workspace};
use anyhow::{bail, Context};
use itertools::Itertools;
/// Perform a system analysis to verify the system install is working correctly.
#[derive(Clone, Debug, Parser)]
pub(crate) struct Doctor {}
impl Doctor {
pub async fn doctor(self) -> Result<StructuredOutput> {
let Ok(workspace) = Workspace::current().await else {
bail!("dx doctor must be run within a cargo workspace!")
};
let rustc_version = &workspace.rustc_version;
let rustc_sysroot = &workspace.sysroot.to_string_lossy();
let rustlib = workspace.sysroot.join("lib").join("rustlib");
// wasm-opt
let wasm_opt_location = crate::wasm_opt::installed_location();
let wasm_opt_message = match wasm_opt_location.clone() {
Some(path) => path.to_string_lossy().to_string(),
None => "not installed".into(),
};
// wasm-bindgen
let wbg_version = workspace.wasm_bindgen_version();
let wbg_version_msg = match wbg_version.clone() {
Some(msg) => msg,
None => "not required".to_string(),
};
let wasm_bindgen_location = match wbg_version {
Some(vers) => match crate::wasm_bindgen::WasmBindgen::new(&vers).get_binary_path() {
Ok(path) => path.to_string_lossy().to_string(),
Err(err) => err.to_string().lines().join(""),
},
None => "not required".to_string(),
};
// extensions
fn has_dioxus_ext(editor_dir: &str) -> anyhow::Result<PathBuf> {
let home = dirs::home_dir().context("no home dir")?;
let exts = home.join(editor_dir).join("extensions");
for dir in exts.read_dir()?.flatten() {
if dir
.file_name()
.to_string_lossy()
.contains("dioxuslabs.dioxus-")
{
return Ok(dir.path());
}
}
bail!("not found")
}
// Editors
let vscode_ext = has_dioxus_ext(".vscode");
let vscode_ext_msg = match vscode_ext.as_ref() {
Ok(path) => path.to_string_lossy().to_string(),
Err(_) => "not found".to_string(),
};
let cursor_ext = has_dioxus_ext(".cursor");
let cursor_ext_msg = match cursor_ext.as_ref() {
Ok(path) => path.to_string_lossy().to_string(),
Err(_) => "not found".to_string(),
};
// Tailwind
let mut tailwindcss = "not found".to_string();
if let Ok(path) = crate::tailwind::TailwindCli::v3().get_binary_path() {
tailwindcss = path.display().to_string();
}
if let Ok(path) = crate::tailwind::TailwindCli::v4().get_binary_path() {
tailwindcss = path.display().to_string();
}
let mut adb = "not found".to_string();
let mut ndk = "not found".to_string();
let mut sdk = "not found".to_string();
let mut java_home = "not found".to_string();
let mut emulator = "not found".to_string();
if let Some(rf) = workspace.android_tools.as_deref() {
if rf.adb.exists() {
adb = rf.adb.display().to_string();
}
if rf.ndk.exists() {
ndk = rf.ndk.display().to_string();
}
if let Some(jh) = rf.java_home.as_ref() {
java_home = jh.display().to_string();
}
if rf.sdk().exists() {
sdk = rf.sdk().display().to_string();
}
if let Some(jh) = rf.java_home.as_ref() {
java_home = jh.display().to_string();
}
if rf.emulator().exists() {
emulator = rf.emulator().display().to_string();
}
};
let mut simulator_location = "not found".to_string();
let mut xcode_install = "not found".to_string();
if let Some(xcode) = workspace.xcode.as_ref() {
let sim_location = xcode.join("Applications").join("Simulator.app");
if sim_location.exists() {
simulator_location = sim_location.display().to_string();
}
if xcode.exists() {
xcode_install = xcode.display().to_string();
}
}
// toolchains
let mut has_wasm32_unknown_unknown = "❌";
let mut has_aarch64_android_linux = "❌";
let mut has_i686_linux_android = "❌";
let mut has_armv7_linux_androideabi = "❌";
let mut has_x86_64_android_linux = "❌";
let mut has_x86_64_apple_ios = "❌";
let mut has_aarch64_apple_ios = "❌";
let mut has_aarch64_apple_ios_sim = "❌";
let mut has_aarch64_apple_darwin = "❌";
if rustlib.join("wasm32-unknown-unknown").exists() {
has_wasm32_unknown_unknown = "✅";
}
if rustlib.join("aarch64-linux-android").exists() {
has_aarch64_android_linux = "✅";
}
if rustlib.join("i686-linux-android").exists() {
has_i686_linux_android = "✅";
}
if rustlib.join("armv7-linux-androideabi").exists() {
has_armv7_linux_androideabi = "✅";
}
if rustlib.join("x86_64-linux-android").exists() {
has_x86_64_android_linux = "✅";
}
if rustlib.join("x86_64-apple-ios").exists() {
has_x86_64_apple_ios = "✅";
}
if rustlib.join("aarch64-apple-ios").exists() {
has_aarch64_apple_ios = "✅";
}
if rustlib.join("aarch64-apple-ios-sim").exists() {
has_aarch64_apple_ios_sim = "✅";
}
if rustlib.join("aarch64-apple-darwin").exists() {
has_aarch64_apple_darwin = "✅";
}
// Things to know
// - current rust version and rust-related things
// - installed toolchains
// -
use crate::styles::*;
println!(
r#"{LINK_STYLE}Setup{LINK_STYLE:#}
{GLOW_STYLE}Web{GLOW_STYLE:#}: wasm-bindgen, wasm-opt, and TailwindCSS are downloaded automatically
{GLOW_STYLE}iOS{GLOW_STYLE:#}: Install iOS SDK and developer tools and through XCode
{GLOW_STYLE}Android{GLOW_STYLE:#}: Install Android Studio, NDK, and then set ANDROID_HOME and ANDROID_NDK_HOME
{GLOW_STYLE}macOS{GLOW_STYLE:#}: all tools should be installed by default
{GLOW_STYLE}Windows{GLOW_STYLE:#}: install the webview2 binary
{GLOW_STYLE}Linux{GLOW_STYLE:#}: Install libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
{LINK_STYLE}Rust{LINK_STYLE:#}
Rustc version: {HINT_STYLE}{rustc_version}{HINT_STYLE:#}
Rustc sysroot: {HINT_STYLE}{rustc_sysroot}{HINT_STYLE:#}
{LINK_STYLE}Devtools{LINK_STYLE:#}
VSCode Extension: {HINT_STYLE}{vscode_ext_msg}{HINT_STYLE:#}
Cursor Extension: {HINT_STYLE}{cursor_ext_msg}{HINT_STYLE:#}
TailwindCSS: {HINT_STYLE}{tailwindcss}{HINT_STYLE:#}
{LINK_STYLE}Web{LINK_STYLE:#}
wasm-opt: {HINT_STYLE}{wasm_opt_message}{HINT_STYLE:#}
wasm-bindgen: {HINT_STYLE}{wasm_bindgen_location}{HINT_STYLE:#}
wasm-bindgen version: {HINT_STYLE}{wbg_version_msg}{HINT_STYLE:#}
{LINK_STYLE}iOS{LINK_STYLE:#}
XCode: {HINT_STYLE}{xcode_install}{HINT_STYLE:#}
Simulator: {HINT_STYLE}{simulator_location}{HINT_STYLE:#}
{LINK_STYLE}Android{LINK_STYLE:#}
sdk: {HINT_STYLE}{sdk}{HINT_STYLE:#}
ndk: {HINT_STYLE}{ndk}{HINT_STYLE:#}
adb: {HINT_STYLE}{adb}{HINT_STYLE:#}
emulator: {HINT_STYLE}{emulator}{HINT_STYLE:#}
java_home: {HINT_STYLE}{java_home}{HINT_STYLE:#}
{LINK_STYLE}Toolchains{LINK_STYLE:#}
{HINT_STYLE}{has_wasm32_unknown_unknown}{HINT_STYLE:#} wasm32-unknown-unknown {HINT_STYLE}(web){HINT_STYLE:#}
{HINT_STYLE}{has_aarch64_android_linux}{HINT_STYLE:#} aarch64-android-linux {HINT_STYLE}(android){HINT_STYLE:#}
{HINT_STYLE}{has_i686_linux_android}{HINT_STYLE:#} i686-linux-android {HINT_STYLE}(android){HINT_STYLE:#}
{HINT_STYLE}{has_armv7_linux_androideabi}{HINT_STYLE:#} armv7-linux-androideabi {HINT_STYLE}(android){HINT_STYLE:#}
{HINT_STYLE}{has_x86_64_android_linux}{HINT_STYLE:#} x86_64-android-linux {HINT_STYLE}(android){HINT_STYLE:#}
{HINT_STYLE}{has_x86_64_apple_ios}{HINT_STYLE:#} x86_64-apple-ios {HINT_STYLE}(iOS){HINT_STYLE:#}
{HINT_STYLE}{has_aarch64_apple_ios}{HINT_STYLE:#} aarch64-apple-ios {HINT_STYLE}(iOS){HINT_STYLE:#}
{HINT_STYLE}{has_aarch64_apple_ios_sim}{HINT_STYLE:#} aarch64-apple-ios-sim {HINT_STYLE}(iOS){HINT_STYLE:#}
{HINT_STYLE}{has_aarch64_apple_darwin}{HINT_STYLE:#} aarch64-apple-darwin {HINT_STYLE}(iOS){HINT_STYLE:#}
Get help: {LINK_STYLE}https://discord.gg/XgGxMSkvUM{LINK_STYLE:#}
More info: {LINK_STYLE}https://dioxuslabs.com/learn/0.6/{LINK_STYLE:#}
"#
);
Ok(StructuredOutput::Success)
}
}
+6 -5
View File
@@ -3,9 +3,9 @@ pub(crate) mod build;
pub(crate) mod build_assets;
pub(crate) mod bundle;
pub(crate) mod check;
pub(crate) mod clean;
pub(crate) mod config;
pub(crate) mod create;
pub(crate) mod doctor;
pub(crate) mod init;
pub(crate) mod link;
pub(crate) mod platform_override;
@@ -75,9 +75,9 @@ pub(crate) enum Commands {
#[clap(name = "init")]
Init(init::Init),
/// Clean output artifacts.
#[clap(name = "clean")]
Clean(clean::Clean),
/// Diagnose installed tools and system configuration.
#[clap(name = "doctor")]
Doctor(doctor::Doctor),
/// Translate a source file into Dioxus code.
#[clap(name = "translate")]
@@ -121,7 +121,6 @@ impl Display for Commands {
Commands::Serve(_) => write!(f, "serve"),
Commands::New(_) => write!(f, "create"),
Commands::Init(_) => write!(f, "init"),
Commands::Clean(_) => write!(f, "clean"),
Commands::Config(_) => write!(f, "config"),
Commands::Autoformat(_) => write!(f, "fmt"),
Commands::Check(_) => write!(f, "check"),
@@ -129,6 +128,7 @@ impl Display for Commands {
Commands::Run(_) => write!(f, "run"),
Commands::SelfUpdate(_) => write!(f, "self-update"),
Commands::Tools(_) => write!(f, "tools"),
Commands::Doctor(_) => write!(f, "doctor"),
}
}
}
@@ -169,4 +169,5 @@ pub mod styles {
pub(crate) const NOTE_STYLE: Style = AnsiColor::Green.on_default();
pub(crate) const LINK_STYLE: Style = AnsiColor::Blue.on_default();
pub(crate) const ERROR_STYLE: Style = AnsiColor::Red.on_default();
pub(crate) const HINT_STYLE: Style = clap::builder::styling::Ansi256Color(244).on_default();
}
+9 -12
View File
@@ -1,8 +1,9 @@
use super::*;
use crate::{
serve::{AppServer, ServeUpdate, WebServer},
BuilderUpdate, Error, Platform, Result,
BuilderUpdate, Platform, Result,
};
use anyhow::bail;
use dioxus_dx_wire_format::BuildStage;
/// Run the project with the given arguments
@@ -29,9 +30,11 @@ impl RunArgs {
self.args.hot_reload = Some(false);
self.args.watch = Some(false);
let mut builder = AppServer::start(self.args).await?;
let mut builder = AppServer::new(self.args).await?;
let mut devserver = WebServer::start(&builder)?;
builder.initialize();
loop {
let msg = tokio::select! {
msg = builder.wait() => msg,
@@ -109,15 +112,11 @@ impl RunArgs {
}
BuildStage::Failed => {
tracing::error!("[{platform}] Build failed");
return Err(Error::Cargo(format!(
"Build failed for platform: {platform}"
)));
bail!("Build failed for platform: {platform}");
}
BuildStage::Aborted => {
tracing::error!("[{platform}] Build aborted");
return Err(Error::Cargo(format!(
"Build aborted for platform: {platform}"
)));
bail!("Build aborted for platform: {platform}");
}
_ => {}
},
@@ -125,7 +124,7 @@ impl RunArgs {
print!("{}", message);
}
BuilderUpdate::BuildFailed { err } => {
tracing::error!("Build failed: {}", err);
tracing::error!("❌ Build failed: {}", err);
return Err(err);
}
BuilderUpdate::StdoutReceived { msg } => {
@@ -139,9 +138,7 @@ impl RunArgs {
tracing::error!(
"Application [{platform}] exited with error: {status}"
);
return Err(Error::Runtime(format!(
"Application [{platform}] exited with error: {status}"
)));
bail!("Application [{platform}] exited with error: {status}");
}
break;
+1 -1
View File
@@ -181,7 +181,7 @@ impl ServeArgs {
})
.unwrap_or_else(|| format!("dx serve panicked: {as_str}"));
Err(crate::error::Error::CapturedPanic(message))
Err(anyhow::anyhow!(message))
}
}
}
+3 -2
View File
@@ -1,5 +1,6 @@
use super::*;
use crate::{Result, StructuredOutput};
use anyhow::bail;
use dioxus_rsx::{BodyNode, CallBody, TemplateBody};
/// Translate some source file into Dioxus code
@@ -108,7 +109,7 @@ fn determine_input(file: Option<String>, raw: Option<String>) -> Result<String>
// Make sure not both are specified
if file.is_some() && raw.is_some() {
return Err("Only one of --file or --raw should be specified.".into());
bail!("Only one of --file or --raw should be specified.");
}
if let Some(raw) = raw {
@@ -121,7 +122,7 @@ fn determine_input(file: Option<String>, raw: Option<String>) -> Result<String>
// If neither exist, we try to read from stdin
if std::io::stdin().is_terminal() {
return Err(anyhow::anyhow!("No input file, source, or stdin to translate from.").into());
bail!("No input file, source, or stdin to translate from.");
}
let mut buffer = String::new();
+5 -8
View File
@@ -1,6 +1,6 @@
use super::*;
use crate::{Result, Workspace};
use anyhow::Context;
use anyhow::{bail, Context};
use itertools::Itertools;
use self_update::cargo_crate_version;
@@ -93,7 +93,7 @@ impl SelfUpdate {
} else if cfg!(target_arch = "aarch64") {
"aarch64"
} else {
return Err(Error::Unique("Unsupported architecture".to_string()));
bail!("Unsupported architecture");
};
let cur_os = if cfg!(target_os = "windows") {
@@ -103,7 +103,7 @@ impl SelfUpdate {
} else if cfg!(target_os = "macos") {
"darwin"
} else {
return Err(Error::Unique("Unsupported OS".to_string()));
bail!("Unsupported OS");
};
let zip_ext = "zip";
@@ -118,7 +118,7 @@ impl SelfUpdate {
&& a.name.contains(cur_arch)
&& a.name.ends_with(zip_ext)
})
.ok_or_else(|| Error::Unique("No suitable release found found".to_string()))?;
.context("No suitable asset found")?;
let install_dir = Workspace::dioxus_home_dir().join("self-update");
std::fs::create_dir_all(&install_dir).context("Failed to create install directory")?;
@@ -161,10 +161,7 @@ impl SelfUpdate {
let executable = install_dir.join("dx");
if !executable.exists() {
return Err(Error::Unique(format!(
"Executable not found in {}",
install_dir.display()
)));
bail!("Executable not found in {}", install_dir.display());
}
tracing::info!(
+25 -69
View File
@@ -1,77 +1,33 @@
use std::fmt::Debug;
use thiserror::Error as ThisError;
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
pub use anyhow::Error;
use itertools::Itertools;
#[derive(ThisError, Debug)]
pub(crate) enum Error {
/// Used when errors need to propagate but are too unique to be typed
#[error("{0}")]
Unique(String),
pub fn log_stacktrace(err: &anyhow::Error, padding: usize) -> String {
let mut trace = format!("{err}",);
#[error("I/O Error: {0}")]
IO(#[from] std::io::Error),
#[error("Format Error: {0}")]
Format(#[from] std::fmt::Error),
#[error("Format failed: {0}")]
Parse(String),
#[error("Runtime Error: {0}")]
Runtime(String),
#[error("Cargo Error: {0}")]
Cargo(String),
#[error("Invalid proxy URL: {0}")]
InvalidProxy(#[from] hyper::http::uri::InvalidUri),
#[error("Establishing proxy: {0}")]
ProxySetup(String),
#[error("Bundling project: {0}")]
BundleFailed(#[from] tauri_bundler::Error),
#[error("Performing hotpatch: {0}")]
PatchingFailed(#[from] crate::build::PatchError),
#[error("Reading object file: {0}")]
ObjectReadFailed(#[from] object::Error),
#[error("{0}")]
CapturedPanic(String),
#[error("Rendering template error: {0}")]
TemplateParse(#[from] handlebars::RenderError),
#[error("Network connectivity error: {0}")]
Network(String),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl From<&str> for Error {
fn from(s: &str) -> Self {
Error::Unique(s.to_string())
for (idx, cause) in err.chain().enumerate().skip(1) {
trace.push_str(&format!(
"\n{}{IDX_STYLE}{idx}{IDX_STYLE:#}: {}",
" ".repeat(padding),
cause
.to_string()
.lines()
.enumerate()
.map(|(idx, line)| {
if idx == 0 {
line.to_string()
} else {
format!("{}{}", " ".repeat(padding + 3), line)
}
})
.join("\n"),
IDX_STYLE = crate::styles::GLOW_STYLE,
));
}
}
impl From<String> for Error {
fn from(s: String) -> Self {
Error::Unique(s)
if crate::VERBOSITY.get().unwrap().trace {
trace.push_str(&format!("\nBacktrace:\n{}", err.backtrace()));
}
}
impl From<html_parser::Error> for Error {
fn from(e: html_parser::Error) -> Self {
Self::Parse(e.to_string())
}
}
impl From<hyper::Error> for Error {
fn from(e: hyper::Error) -> Self {
Self::Runtime(e.to_string())
}
trace
}
+7 -6
View File
@@ -56,13 +56,13 @@ async fn main() {
Commands::Config(opts) => opts.config().await,
Commands::Autoformat(opts) => opts.autoformat().await,
Commands::Check(opts) => opts.check().await,
Commands::Clean(opts) => opts.clean().await,
Commands::Build(opts) => opts.build().await,
Commands::Serve(opts) => opts.serve().await,
Commands::Bundle(opts) => opts.bundle().await,
Commands::Run(opts) => opts.run().await,
Commands::SelfUpdate(opts) => opts.self_update().await,
Commands::Tools(BuildTools::BuildAssets(opts)) => opts.run().await,
Commands::Doctor(opts) => opts.doctor().await,
};
// Provide a structured output for third party tools that can consume the output of the CLI
@@ -71,17 +71,18 @@ async fn main() {
tracing::debug!(json = ?output);
}
Err(err) => {
eprintln!(
"{ERROR_STYLE}failed:{ERROR_STYLE:#} {err}",
ERROR_STYLE = styles::ERROR_STYLE
);
tracing::error!(
json = ?StructuredOutput::Error {
message: format!("{err:?}"),
},
);
eprintln!(
"{ERROR_STYLE}Failed{ERROR_STYLE:#}: {}",
crate::error::log_stacktrace(&err, 1),
ERROR_STYLE = crate::styles::ERROR_STYLE,
);
std::process::exit(1);
}
};
+22 -16
View File
@@ -1,7 +1,6 @@
use crate::{
styles::{GLOW_STYLE, LINK_STYLE},
AppBuilder, BuildId, BuildMode, BuilderUpdate, Error, Platform, Result, ServeArgs,
TraceController,
AppBuilder, BuildId, BuildMode, BuilderUpdate, Platform, Result, ServeArgs, TraceController,
};
mod ansi_buffer;
@@ -12,6 +11,7 @@ mod runner;
mod server;
mod update;
use anyhow::bail;
use dioxus_dx_wire_format::BuildStage;
pub(crate) use output::*;
pub(crate) use runner::*;
@@ -41,7 +41,7 @@ pub(crate) use update::*;
pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> Result<()> {
// Load the args into a plan, resolving all tooling, build dirs, arguments, decoding the multi-target, etc
let exit_on_error = args.exit_on_error;
let mut builder = AppServer::start(args).await?;
let mut builder = AppServer::new(args).await?;
let mut devserver = WebServer::start(&builder)?;
let mut screen = Output::start(builder.interactive).await?;
@@ -66,6 +66,8 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) ->
}
);
builder.initialize();
loop {
// Draw the state of the server to the screen
screen.render(&builder, &devserver);
@@ -145,18 +147,14 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) ->
stage: BuildStage::Failed,
} => {
if exit_on_error {
return Err(Error::Cargo(format!(
"Build failed for platform: {platform}"
)));
bail!("Build failed for platform: {platform}");
}
}
BuilderUpdate::Progress {
stage: BuildStage::Aborted,
} => {
if exit_on_error {
return Err(Error::Cargo(format!(
"Build aborted for platform: {platform}"
)));
bail!("Build aborted for platform: {platform}");
}
}
BuilderUpdate::Progress { .. } => {}
@@ -164,7 +162,12 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) ->
screen.push_cargo_log(message);
}
BuilderUpdate::BuildFailed { err } => {
tracing::error!("Build failed: {}", err);
tracing::error!(
"{ERROR_STYLE}Build failed{ERROR_STYLE:#}: {}",
crate::error::log_stacktrace(&err, 15),
ERROR_STYLE = crate::styles::ERROR_STYLE,
);
if exit_on_error {
return Err(err);
}
@@ -184,7 +187,9 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) ->
Err(err) => {
tracing::error!("Failed to hot-patch app: {err}");
if matches!(err, crate::Error::PatchingFailed(_)) {
if let Some(_patching) =
err.downcast_ref::<crate::build::PatchError>()
{
tracing::info!("Starting full rebuild: {err}");
builder.full_rebuild().await;
devserver.send_reload_start().await;
@@ -216,9 +221,7 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) ->
} else {
tracing::error!("Application [{platform}] exited with error: {status}");
if exit_on_error {
return Err(Error::Runtime(format!(
"Application [{platform}] exited with error: {status}"
)));
bail!("Application [{platform}] exited with error: {status}");
}
}
}
@@ -248,7 +251,10 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) ->
}
_ => {
if let Err(err) = builder.open_all(&devserver, true).await {
tracing::error!("Failed to open app: {err}")
tracing::error!(
"Failed to open app: {}",
crate::error::log_stacktrace(&err, 15)
)
}
}
},
@@ -278,7 +284,7 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) ->
_ = devserver.shutdown().await;
match error {
Some(err) => return Err(anyhow::anyhow!("{}", err).into()),
Some(err) => return Err(err),
None => return Ok(()),
}
}
+3 -3
View File
@@ -29,7 +29,7 @@ use tracing::Level;
use super::AppServer;
const TICK_RATE_MS: u64 = 100;
const VIEWPORT_MAX_WIDTH: u16 = 100;
const VIEWPORT_MAX_WIDTH: u16 = 90;
const VIEWPORT_HEIGHT_SMALL: u16 = 5;
const VIEWPORT_HEIGHT_BIG: u16 = 13;
@@ -204,7 +204,7 @@ impl Output {
Ok(Some(update)) => return update,
Err(ee) => {
return ServeUpdate::Exit {
error: Some(Box::new(ee)),
error: Some(anyhow::anyhow!(ee)),
}
}
Ok(None) => {}
@@ -675,7 +675,7 @@ impl Output {
if client.build.platform == Platform::Web {
"Serving at: ".gray()
} else {
"ServerFns at: ".gray()
"Server at: ".gray()
},
address,
])),
+4 -21
View File
@@ -2,7 +2,7 @@ use crate::config::WebProxyConfig;
use crate::TraceSrc;
use crate::{Error, Result};
use anyhow::{anyhow, Context};
use anyhow::{bail, Context};
use axum::body::Body;
use axum::http::request::Parts;
use axum::{body::Body as MyBody, response::IntoResponse};
@@ -47,7 +47,7 @@ impl ProxyClient {
self.inner
.request(req)
.await
.map_err(|err| crate::error::Error::Other(anyhow!(err)))
.context("Failed to send proxy request")
}
}
@@ -64,11 +64,11 @@ pub(crate) fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Ro
let trimmed_path = path.trim_start_matches('/');
if trimmed_path.is_empty() {
return Err(crate::Error::ProxySetup(format!(
bail!(
"Proxy backend URL must have a non-empty path, e.g. {}/api instead of {}",
proxy.backend.trim_end_matches('/'),
proxy.backend
)));
);
}
let method_router = proxy_to(url, false, handle_proxy_error);
@@ -268,21 +268,4 @@ mod test {
async fn add_proxy_trailing_slash() {
test_proxy_requests("/api/".to_string()).await;
}
#[test]
fn add_proxy_empty_path() {
let config = WebProxyConfig {
backend: "http://localhost:8000".to_string(),
};
let router = super::add_proxy(Router::new(), &config);
match router.unwrap_err() {
crate::Error::ProxySetup(e) => {
assert_eq!(
e,
"Proxy backend URL must have a non-empty path, e.g. http://localhost:8000/api instead of http://localhost:8000"
);
}
e => panic!("Unexpected error type: {}", e),
}
}
}
+40 -17
View File
@@ -1,10 +1,10 @@
use super::{AppBuilder, ServeUpdate, WebServer};
use crate::{
platform_override::CommandWithPlatformOverrides, BuildArtifacts, BuildId, BuildMode,
BuildTargets, BuilderUpdate, Error, HotpatchModuleCache, Platform, Result, ServeArgs,
TailwindCli, TraceSrc, Workspace,
BuildTargets, BuilderUpdate, HotpatchModuleCache, Platform, Result, ServeArgs, TailwindCli,
TraceSrc, Workspace,
};
use anyhow::Context;
use anyhow::{bail, Context};
use dioxus_core::internal::{
HotReloadTemplateWithLocation, HotReloadedTemplate, TemplateGlobalKey,
};
@@ -92,7 +92,7 @@ pub(crate) struct CachedFile {
impl AppServer {
/// Create the AppRunner and then initialize the filemap with the crate directory.
pub(crate) async fn start(args: ServeArgs) -> Result<Self> {
pub(crate) async fn new(args: ServeArgs) -> Result<Self> {
let workspace = Workspace::current().await?;
// Resolve the simpler args
@@ -115,7 +115,8 @@ impl AppServer {
let open_browser = args
.open
.unwrap_or_else(|| workspace.settings.always_open_browser.unwrap_or_default());
.unwrap_or_else(|| workspace.settings.always_open_browser.unwrap_or(true))
&& interactive;
let wsl_file_poll_interval = args
.wsl_file_poll_interval
@@ -161,15 +162,9 @@ impl AppServer {
let watch_fs = args.watch.unwrap_or(true);
let use_hotpatch_engine = args.hot_patch;
let build_mode = match use_hotpatch_engine {
true => BuildMode::Fat,
false => BuildMode::Base,
};
let client = AppBuilder::start(&client, build_mode.clone())?;
let server = server
.map(|server| AppBuilder::start(&server, build_mode))
.transpose()?;
let client = AppBuilder::new(&client)?;
let server = server.map(|server| AppBuilder::new(&server)).transpose()?;
let tw_watcher = TailwindCli::serve(
client.build.package_manifest_dir(),
@@ -229,6 +224,18 @@ impl AppServer {
Ok(runner)
}
pub(crate) fn initialize(&mut self) {
let build_mode = match self.use_hotpatch_engine {
true => BuildMode::Fat,
false => BuildMode::Base,
};
self.client.start(build_mode.clone());
if let Some(server) = self.server.as_mut() {
server.start(build_mode);
}
}
pub(crate) async fn rebuild_ssg(&mut self, devserver: &WebServer) {
if self.client.stage != BuildStage::Success {
return;
@@ -355,9 +362,10 @@ impl AppServer {
for path in files {
// for various assets that might be linked in, we just try to hotreloading them forcefully
// That is, unless they appear in an include! macro, in which case we need to a full rebuild....
let Some(ext) = path.extension().and_then(|v| v.to_str()) else {
continue;
};
let ext = path
.extension()
.and_then(|v| v.to_str())
.unwrap_or_default();
// If it's an asset, we want to hotreload it
// todo(jon): don't hardcode this here
@@ -458,6 +466,21 @@ impl AppServer {
}
}
}
// If it's not a rust file, then it might be depended on via include! or similar
if ext != "rs" {
if let Some(artifacts) = self.client.artifacts.as_ref() {
if artifacts.depinfo.files.contains(path) {
needs_full_rebuild = true;
break;
}
}
}
}
// If the client is in a failed state, any changes to rsx should trigger a rebuild/hotpatch
if self.client.stage == BuildStage::Failed && !templates.is_empty() {
needs_full_rebuild = true
}
// todo - we need to distinguish between hotpatchable rebuilds and true full rebuilds.
@@ -685,7 +708,7 @@ impl AppServer {
.hotpatch(res, cache)
.await
}
_ => return Err(Error::Runtime("Invalid build id".into())),
_ => bail!("Invalid build id"),
}?;
if id == BuildId::CLIENT {
+3 -4
View File
@@ -2,7 +2,7 @@ use crate::{
config::WebHttpsConfig, serve::ServeUpdate, BuildId, BuildStage, BuilderUpdate, Platform,
Result, TraceSrc,
};
use anyhow::Context;
use anyhow::{bail, Context};
use axum::{
body::Body,
extract::{
@@ -635,8 +635,7 @@ async fn get_rustls(web_config: &WebHttpsConfig) -> Result<(String, String)> {
{
return Ok((cert, key));
} else {
// missing cert or key
return Err("https is enabled but cert or key path is missing".into());
bail!("https is enabled but cert or key path is missing");
}
}
@@ -682,7 +681,7 @@ async fn get_rustls(web_config: &WebHttpsConfig) -> Result<(String, String)> {
tracing::error!(dx_src = ?TraceSrc::Dev, "An error occurred while generating mkcert certificates: {}", e.to_string())
}
};
return Err("failed to generate mkcert certificates".into());
bail!("failed to generate mkcert certificates");
}
Ok(mut cmd) => {
cmd.wait().await?;
+2 -2
View File
@@ -1,4 +1,4 @@
use crate::{BuildId, BuilderUpdate, Platform, TraceMsg};
use crate::{BuildId, BuilderUpdate, Error, Platform, TraceMsg};
use axum::extract::ws::Message as WsMessage;
use std::path::PathBuf;
@@ -44,6 +44,6 @@ pub(crate) enum ServeUpdate {
},
Exit {
error: Option<Box<dyn std::error::Error + Send + Sync>>,
error: Option<Error>,
},
}
+3 -4
View File
@@ -1,4 +1,5 @@
use crate::{Result, TraceSrc};
use anyhow::bail;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
use std::{fs, path::PathBuf, sync::Arc};
@@ -89,16 +90,14 @@ impl CliSettings {
?path,
"failed to create directories for settings file"
);
return Err(
anyhow::anyhow!("failed to create directories for settings file: {e}").into(),
);
bail!("failed to create directories for settings file: {e}");
}
// Write the data.
let result = fs::write(&path, data.clone());
if let Err(e) = result {
error!(?data, ?path, "failed to save global cli settings");
return Err(anyhow::anyhow!("failed to save global cli settings: {e}").into());
bail!("failed to save global cli settings: {e}");
}
Ok(())
+2 -1
View File
@@ -141,6 +141,7 @@ impl TailwindCli {
.arg("--output")
.arg(output_path)
.args(watch.then_some("--watch"))
.current_dir(manifest_dir)
.kill_on_drop(true)
.stdin(Stdio::piped())
.stdout(Stdio::null())
@@ -150,7 +151,7 @@ impl TailwindCli {
Ok(proc)
}
fn get_binary_path(&self) -> anyhow::Result<PathBuf> {
pub fn get_binary_path(&self) -> anyhow::Result<PathBuf> {
if CliSettings::prefer_no_downloads() {
which::which("tailwindcss").map_err(|_| anyhow!("Missing tailwindcss@{}", self.version))
} else {
+17 -19
View File
@@ -4,7 +4,7 @@ use flate2::read::GzDecoder;
use std::path::{Path, PathBuf};
use tar::Archive;
use tempfile::TempDir;
use tokio::{fs, process::Command};
use tokio::process::Command;
pub(crate) struct WasmBindgen {
version: String,
@@ -100,7 +100,7 @@ impl WasmBindgen {
/// Run the bindgen command with the current settings
pub(crate) async fn run(&self) -> Result<std::process::Output> {
let binary = self.get_binary_path().await?;
let binary = self.get_binary_path()?;
let mut args = Vec::new();
@@ -253,7 +253,7 @@ impl WasmBindgen {
})?;
// Get the final binary location.
let binary_path = self.get_binary_path().await?;
let binary_path = self.get_binary_path()?;
// Download then extract wasm-bindgen-cli.
let bytes = reqwest::get(url).await?.bytes().await?;
@@ -300,11 +300,10 @@ impl WasmBindgen {
.output()
.await?;
fs::copy(
std::fs::copy(
tempdir.path().join(self.downloaded_bin_name()),
self.get_binary_path().await?,
)
.await?;
self.get_binary_path()?,
)?;
Ok(())
}
@@ -336,11 +335,10 @@ impl WasmBindgen {
tracing::info!("Copying into path: {}", tempdir.path().display());
// copy the wasm-bindgen out of the tempdir to the final location
fs::copy(
std::fs::copy(
tempdir.path().join("bin").join(self.downloaded_bin_name()),
self.get_binary_path().await?,
self.get_binary_path()?,
)
.await
.context("failed to copy wasm-bindgen binary")?;
Ok(())
@@ -352,7 +350,7 @@ impl WasmBindgen {
self.version
);
let binary = self.get_binary_path().await?;
let binary = self.get_binary_path()?;
let output = Command::new(binary)
.args(["--version"])
.output()
@@ -381,7 +379,7 @@ impl WasmBindgen {
);
let binary_name = self.installed_bin_name();
let path = self.install_dir().await?.join(binary_name);
let path = self.install_dir()?.join(binary_name);
if !path.exists() {
self.install().await?;
@@ -390,20 +388,20 @@ impl WasmBindgen {
Ok(())
}
async fn get_binary_path(&self) -> anyhow::Result<PathBuf> {
pub fn get_binary_path(&self) -> anyhow::Result<PathBuf> {
if CliSettings::prefer_no_downloads() {
which::which("wasm-bindgen")
.map_err(|_| anyhow!("Missing wasm-bindgen-cli@{}", self.version))
} else {
let installed_name = self.installed_bin_name();
let install_dir = self.install_dir().await?;
let install_dir = self.install_dir()?;
Ok(install_dir.join(installed_name))
}
}
async fn install_dir(&self) -> anyhow::Result<PathBuf> {
fn install_dir(&self) -> anyhow::Result<PathBuf> {
let bindgen_dir = Workspace::dioxus_home_dir().join("wasm-bindgen/");
fs::create_dir_all(&bindgen_dir).await?;
std::fs::create_dir_all(&bindgen_dir)?;
Ok(bindgen_dir)
}
@@ -501,7 +499,7 @@ mod test {
/// Helper to test that the installed binary actually exists.
async fn verify_installation(binary: &WasmBindgen) {
let path = binary.install_dir().await.unwrap();
let path = binary.install_dir().unwrap();
let name = binary.installed_bin_name();
let binary_path = path.join(name);
assert!(
@@ -513,9 +511,9 @@ mod test {
/// Delete the installed binary. The temp folder should be automatically deleted.
async fn reset_test() {
let binary = WasmBindgen::new(VERSION);
let path = binary.install_dir().await.unwrap();
let path = binary.install_dir().unwrap();
let name = binary.installed_bin_name();
let binary_path = path.join(name);
let _ = tokio::fs::remove_file(binary_path).await;
let _ = std::fs::remove_file(binary_path);
}
}
+31 -13
View File
@@ -1,6 +1,6 @@
use crate::config::WasmOptLevel;
use crate::{CliSettings, Result, WasmOptConfig, Workspace};
use anyhow::{anyhow, Context};
use anyhow::{anyhow, bail, Context};
use flate2::read::GzDecoder;
use std::path::{Path, PathBuf};
use tar::Archive;
@@ -16,8 +16,11 @@ pub async fn write_wasm(bytes: &[u8], output_path: &Path, cfg: &WasmOptConfig) -
pub async fn optimize(input_path: &Path, output_path: &Path, cfg: &WasmOptConfig) -> Result<()> {
let wasm_opt = WasmOpt::new(input_path, output_path, cfg)
.await
.inspect_err(|err| tracing::error!("Failed to create wasm-opt instance: {}", err))?;
wasm_opt.optimize().await?;
.context("Failed to create wasm-opt instance")?;
wasm_opt
.optimize()
.await
.context("Failed to run wasm-opt")?;
Ok(())
}
@@ -170,9 +173,7 @@ async fn find_latest_wasm_opt_download_url() -> anyhow::Result<String> {
} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
"arm64-macos"
} else {
return Err(anyhow::anyhow!(
"Unknown platform for wasm-opt installation. Please install wasm-opt manually from https://github.com/WebAssembly/binaryen/releases and add it to your PATH."
));
bail!("Unknown platform for wasm-opt installation. Please install wasm-opt manually from https://github.com/WebAssembly/binaryen/releases and add it to your PATH.");
};
// Find the first asset with a name that contains the platform string
@@ -184,12 +185,10 @@ async fn find_latest_wasm_opt_download_url() -> anyhow::Result<String> {
.and_then(|name| name.as_str())
.is_some_and(|name| name.contains(platform) && !name.ends_with("sha256"))
})
.ok_or_else(|| {
anyhow::anyhow!(
"No suitable wasm-opt binary found for platform: {}. Please install wasm-opt manually from https://github.com/WebAssembly/binaryen/releases and add it to your PATH.",
platform
)
})?;
.with_context(|| anyhow!(
"No suitable wasm-opt binary found for platform: {}. Please install wasm-opt manually from https://github.com/WebAssembly/binaryen/releases and add it to your PATH.",
platform
))?;
// Extract the download URL from the asset
let download_url = asset
@@ -201,7 +200,7 @@ async fn find_latest_wasm_opt_download_url() -> anyhow::Result<String> {
}
/// Get the path to the wasm-opt binary, downloading it if necessary
async fn get_binary_path() -> anyhow::Result<PathBuf> {
pub async fn get_binary_path() -> anyhow::Result<PathBuf> {
let install_dir = install_dir();
let install_path = installed_bin_path(&install_dir);
@@ -224,6 +223,25 @@ async fn get_binary_path() -> anyhow::Result<PathBuf> {
Ok(install_path)
}
pub fn installed_location() -> Option<PathBuf> {
let install_dir = install_dir();
let install_path = installed_bin_path(&install_dir);
if install_path.exists() {
return Some(install_path);
}
if CliSettings::prefer_no_downloads() {
if let Ok(existing) = which::which("wasm-opt") {
return Some(existing);
} else {
return None;
}
}
None
}
fn install_dir() -> PathBuf {
Workspace::dioxus_home_dir().join("binaryen")
}
+39 -23
View File
@@ -2,7 +2,7 @@ use crate::styles::GLOW_STYLE;
use crate::CliSettings;
use crate::Result;
use crate::{config::DioxusConfig, AndroidTools};
use anyhow::Context;
use anyhow::{bail, Context};
use ignore::gitignore::Gitignore;
use krates::{semver::Version, KrateDetails, LockOptions};
use krates::{Cmd, Krates, NodeId};
@@ -21,6 +21,7 @@ pub struct Workspace {
pub(crate) ignore: Gitignore,
pub(crate) cargo_toml: cargo_toml::Manifest,
pub(crate) android_tools: Option<Arc<AndroidTools>>,
pub(crate) xcode: Option<PathBuf>,
}
impl Workspace {
@@ -48,9 +49,7 @@ impl Workspace {
let mut builder = krates::Builder::new();
builder.workspace(true);
let res = builder
.build(cmd, |_| {})
.context("Failed to run cargo metadata");
let res = builder.build(cmd, |_| {})?;
if !lock_options.offline {
if let Ok(res) = std::env::var("SIMULATE_SLOW_NETWORK") {
@@ -58,7 +57,7 @@ impl Workspace {
}
}
res
Ok(res) as Result<Krates, krates::Error>
});
let spin_future = async move {
@@ -76,10 +75,14 @@ impl Workspace {
};
let krates = tokio::select! {
f = krates_future => f.context("failed to run cargo metadata")??,
_ = spin_future => return Err(crate::Error::Network(
"cargo metadata took too long to respond, try again with --offline".to_string(),
)),
f = krates_future => {
let res = f?;
if let Err(krates::Error::Metadata(e)) = res {
bail!("{e}");
}
res?
},
_ = spin_future => bail!("cargo metadata took too long to respond, try again with --offline"),
};
let settings = CliSettings::global_or_default();
@@ -107,6 +110,13 @@ impl Workspace {
let android_tools = crate::build::get_android_tools();
let xcode = Command::new("xcode-select")
.arg("-p")
.output()
.await
.ok()
.map(|s| String::from_utf8_lossy(&s.stdout).trim().to_string().into());
let workspace = Arc::new(Self {
krates,
settings,
@@ -116,6 +126,7 @@ impl Workspace {
ignore,
cargo_toml,
android_tools,
xcode,
});
tracing::debug!(
@@ -141,10 +152,10 @@ impl Workspace {
}
pub fn android_tools(&self) -> Result<Arc<AndroidTools>> {
Ok(self
self
.android_tools
.clone()
.context("Android not installed properly. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation.")?)
.context("Android not installed properly. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation.")
}
pub fn is_release_profile(&self, profile: &str) -> bool {
@@ -195,7 +206,10 @@ impl Workspace {
let max = dioxus_versions.iter().max().unwrap();
// If the minimum dioxus version is greater than the current cli version, warn the user
if min > &dx_semver || max < &dx_semver {
if min > &dx_semver
|| max < &dx_semver
|| dioxus_versions.iter().any(|f| f.pre != dx_semver.pre)
{
tracing::error!(
r#"🚫dx and dioxus versions are incompatible!
• dx version: {dx_semver}
@@ -249,6 +263,14 @@ impl Workspace {
self.gcc_ld_dir().join("wasm-ld")
}
/// Return the version of the wasm-bindgen crate if it exists
pub fn wasm_bindgen_version(&self) -> Option<String> {
self.krates
.krates_by_name("wasm-bindgen")
.next()
.map(|krate| krate.krate.version.to_string())
}
// wasm-ld: ./rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/wasm-ld
// rust-lld: ./rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/rust-lld
fn gcc_ld_dir(&self) -> PathBuf {
@@ -377,7 +399,7 @@ impl Workspace {
toml::from_str::<DioxusConfig>(&std::fs::read_to_string(&dioxus_conf_file)?)
.map_err(|err| {
anyhow::anyhow!("Failed to parse Dioxus.toml at {dioxus_conf_file:?}: {err}").into()
anyhow::anyhow!("Failed to parse Dioxus.toml at {dioxus_conf_file:?}: {err}")
})
.map(Some)
}
@@ -459,24 +481,18 @@ impl Workspace {
}
None
})
.ok_or_else(|| {
crate::Error::Cargo("Failed to find directory containing Cargo.toml".to_string())
})
.context("Failed to find directory containing Cargo.toml")
}
/// Returns the properly canonicalized path to the dx executable, used for linking and wrapping rustc
pub(crate) fn path_to_dx() -> Result<PathBuf> {
Ok(
dunce::canonicalize(std::env::current_exe().context("Failed to find dx")?)
.context("Failed to find dx")?,
)
dunce::canonicalize(std::env::current_exe().context("Failed to find dx")?)
.context("Failed to find dx")
}
/// Returns the path to the dioxus home directory, used to install tools and other things
pub(crate) fn dioxus_home_dir() -> PathBuf {
dirs::data_local_dir()
.map(|f| f.join("dioxus/"))
.unwrap_or_else(|| dirs::home_dir().unwrap().join(".dioxus"))
dirs::home_dir().unwrap().join(".dioxus")
}
}