CLI self-updater, warn on version mismatch, direct install.sh option, androidmanifest and info.plist customization, bump to 0.7-alpha.0 (#4095)

* wip.... bun installer

* - warn on incompatible versions
- clean up some logs
- add coloring to log levels

* add toasts to desktop/mobile

* Auto open simulators

* auto boot android

* preserve url

* pull out some of the warnings

* add self-updater

* wip: mcp

* add install.sh and adjust binstall path

* add some cuter styling to logs!

* update "input.css"

* update other input.css

* style a bit more

* change getting started

* add windows installer

* move dioxus-mobile into dioxus-desktop
add sentry wip to cli

* shorten timeout of toast

* fix cli display

* wire up customization of android manifest

* udid is a real word

* add 429 to lychee cache

* accept all 200-204 and 429
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-05-14 08:43:11 +02:00
1 parent 53ef78823d
commit 4d3d55a7a3
56 files changed
+1560 -548

No files matched your search

+57 -30
View File
@@ -4,7 +4,7 @@ use crate::{
};
use anyhow::Context;
use dioxus_cli_opt::process_file_to;
use futures_util::future::OptionFuture;
use futures_util::{future::OptionFuture, pin_mut, FutureExt};
use std::{
env,
time::{Duration, Instant, SystemTime},
@@ -617,13 +617,15 @@ impl AppBuilder {
_ => vec![],
};
use crate::styles::{GLOW_STYLE, NOTE_STYLE};
let changed_file = changed_files.first().unwrap();
tracing::info!(
"Hot-patching: {} took {:?}ms",
"Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}",
changed_file
.strip_prefix(std::env::current_dir().unwrap())
.unwrap_or(changed_file.as_path())
.display(),
.display()
.to_string()
.trim_start_matches(&self.build.crate_dir().display().to_string()),
SystemTime::now()
.duration_since(res.time_start)
.unwrap()
@@ -704,7 +706,7 @@ impl AppBuilder {
let target = dioxus_cli_config::android_session_cache_dir().join(bundled_name);
tracing::debug!("Pushing asset to device: {target:?}");
let res = tokio::process::Command::new(&self.build.workspace.android_tools()?.adb)
let res = Command::new(&self.build.workspace.android_tools()?.adb)
.arg("push")
.arg(changed_file)
.arg(&target)
@@ -1140,7 +1142,7 @@ We checked the folder: {}
let adb = self.build.workspace.android_tools()?.adb.clone();
// Start backgrounded since .open() is called while in the arm of the top-level match
tokio::task::spawn(async move {
let _handle: JoinHandle<Result<()>> = tokio::task::spawn(async move {
// call `adb root` so we can push patches to the device
if root {
if let Err(e) = Command::new(&adb).arg("root").output().await {
@@ -1159,54 +1161,79 @@ We checked the folder: {}
tracing::error!("failed to forward port {port}: {e}");
}
// Wait for device to be ready
let cmd = Command::new(&adb)
.arg("wait-for-device")
.arg("shell")
.arg(r#"while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;"#)
.output();
let cmd_future = cmd.fuse();
pin_mut!(cmd_future);
tokio::select! {
_ = &mut cmd_future => {}
_ = tokio::time::sleep(Duration::from_millis(50)) => {
tracing::info!("Waiting for android emulator to be ready...");
_ = cmd_future.await;
}
}
// Install
// adb install -r app-debug.apk
if let Err(e) = Command::new(&adb)
let res = Command::new(&adb)
.arg("install")
.arg("-r")
.arg(apk_path)
.output()
.await
{
tracing::error!("Failed to install apk with `adb`: {e}");
};
.await?;
let std_err = String::from_utf8_lossy(&res.stderr);
if !std_err.is_empty() {
tracing::error!("Failed to install apk with `adb`: {std_err}");
}
// Clear the session cache dir on the device
Command::new(&adb)
.arg("shell")
.arg("rm")
.arg("-rf")
.arg(dioxus_cli_config::android_session_cache_dir())
.output()
.await?;
// Write the env vars to a .env file in our session cache
let env_file = session_cache.join(".env");
let contents: String = envs
.iter()
.map(|(key, value)| format!("{key}={value}"))
.collect::<Vec<_>>()
.join("\n");
_ = std::fs::write(&env_file, contents);
_ = std::fs::write(
&env_file,
envs.iter()
.map(|(key, value)| format!("{key}={value}"))
.collect::<Vec<_>>()
.join("\n"),
);
// Push the env file to the device
if let Err(e) = tokio::process::Command::new(&adb)
Command::new(&adb)
.arg("push")
.arg(env_file)
.arg(dioxus_cli_config::android_session_cache_dir().join(".env"))
.output()
.await
.context("Failed to push asset to device")
{
tracing::error!("Failed to push .env file to device: {e}");
}
.await?;
// eventually, use the user's MainActivity, not our MainActivity
// adb shell am start -n dev.dioxus.main/dev.dioxus.main.MainActivity
let activity_name = format!("{}/dev.dioxus.main.MainActivity", full_mobile_app_name,);
if let Err(e) = Command::new(&adb)
let res = Command::new(&adb)
.arg("shell")
.arg("am")
.arg("start")
.arg("-n")
.arg(activity_name)
.output()
.await
{
tracing::error!("Failed to start app with `adb`: {e}");
};
.await?;
let std_err = String::from_utf8_lossy(res.stderr.trim_ascii());
if !std_err.is_empty() {
tracing::error!("Failed to start app with `adb`: {std_err}");
}
Ok(())
});
Ok(())
+189 -45
View File
@@ -329,7 +329,7 @@ use manganis::{AssetOptions, JsAssetOptions};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use serde::{Deserialize, Serialize};
use std::{
collections::HashSet,
collections::{BTreeMap, HashSet},
io::Write,
path::{Path, PathBuf},
process::Stdio,
@@ -370,7 +370,7 @@ pub(crate) struct BuildRequest {
pub(crate) platform: Platform,
pub(crate) enabled_platforms: Vec<Platform>,
pub(crate) triple: Triple,
pub(crate) _device: bool,
pub(crate) device: bool,
pub(crate) package: String,
pub(crate) features: Vec<String>,
pub(crate) extra_cargo_args: Vec<String>,
@@ -536,6 +536,10 @@ impl BuildRequest {
// We collect all the platforms it enables first and then select based on the --platform arg
let enabled_platforms =
Self::enabled_cargo_toml_platforms(main_package, args.no_default_features);
let using_dioxus_explicitly = main_package
.dependencies
.iter()
.any(|dep| dep.name == "dioxus");
let mut features = args.features.clone();
let mut no_default_features = args.no_default_features;
@@ -552,6 +556,7 @@ impl BuildRequest {
platform
}
},
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()),
1 => enabled_platforms[0],
@@ -565,7 +570,9 @@ impl BuildRequest {
};
// Add any features required to turn on the client
features.push(Self::feature_for_platform(main_package, platform));
if using_dioxus_explicitly {
features.push(Self::feature_for_platform(main_package, platform));
}
// Set the profile of the build if it's not already set
// This is mostly used for isolation of builds (preventing thrashing) but also useful to have multiple performance profiles
@@ -666,10 +673,10 @@ impl BuildRequest {
tracing::debug!(
r#"Log Files:
link_args_file: {},
link_err_file: {},
rustc_wrapper_args_file: {},
session_cache_dir: {}"#,
• link_args_file: {},
• link_err_file: {},
• rustc_wrapper_args_file: {},
• session_cache_dir: {}"#,
link_args_file.path().display(),
link_err_file.path().display(),
rustc_wrapper_args_file.path().display(),
@@ -684,7 +691,7 @@ session_cache_dir: {}"#,
crate_target,
profile,
triple,
_device: device,
device,
workspace,
config,
enabled_platforms,
@@ -886,7 +893,8 @@ session_cache_dir: {}"#,
let time_end = SystemTime::now();
let mode = ctx.mode.clone();
let platform = self.platform;
tracing::debug!("Build completed successfully - output location: {:?}", exe);
tracing::debug!("Build completed successfully: {:?}", exe);
Ok(BuildArtifacts {
time_end,
@@ -905,8 +913,6 @@ session_cache_dir: {}"#,
/// This uses "known paths" that have stayed relatively stable during cargo's lifetime.
/// One day this system might break and we might need to go back to using the linker approach.
fn collect_assets(&self, exe: &Path, ctx: &BuildContext) -> Result<AssetManifest> {
tracing::debug!("Collecting assets from exe at {} ...", exe.display());
// walk every file in the incremental cache dir, reading and inserting items into the manifest.
let mut manifest = AssetManifest::default();
@@ -1151,7 +1157,7 @@ session_cache_dir: {}"#,
//
// Many args are passed twice, too, which can be confusing, but generally don't have any real
// effect. Note that on macos/ios, there's a special macho header that needs to be set, otherwise
// dyld will complain.a
// dyld will complain.
//
// Also, some flags in darwin land might become deprecated, need to be super conservative:
// - https://developer.apple.com/forums/thread/773907
@@ -1223,7 +1229,6 @@ session_cache_dir: {}"#,
// And now we can run the linker with our new args
let linker = self.select_linker()?;
let out_exe = self.patch_exe(artifacts.time_start);
let out_arg = match self.triple.operating_system {
OperatingSystem::Windows => vec![format!("/OUT:{}", out_exe.display())],
@@ -1301,6 +1306,13 @@ session_cache_dir: {}"#,
//
// We don't use *any* of the original linker args since they do lots of custom exports
// and other things that we don't need.
//
// The trickiest one here is -Crelocation-model=pic, which forces data symbols
// into a GOT, making it possible to import them from the main module.
//
// I think we can make relocation-model=pic work for non-wasm platforms, enabling
// fully relocatable modules with no host coordination in lieu of sending out
// the aslr slide at runtime.
OperatingSystem::Unknown if self.platform == Platform::Web => {
out_args.extend([
"--fatal-warnings".to_string(),
@@ -1390,7 +1402,6 @@ session_cache_dir: {}"#,
"/PDBALTPATH:%_PDB%".to_string(),
"/EXPORT:main".to_string(),
"/HIGHENTROPYVA:NO".to_string(),
// "/SUBSYSTEM:WINDOWS".to_string(),
]);
}
@@ -1764,7 +1775,6 @@ session_cache_dir: {}"#,
cmd.current_dir(self.workspace_dir());
cmd.env_clear();
cmd.args(rustc_args.args[1..].iter());
cmd.envs(rustc_args.envs.iter().cloned());
cmd.env_remove("RUSTC_WORKSPACE_WRAPPER");
cmd.env_remove("RUSTC_WRAPPER");
cmd.env_remove(DX_RUSTC_WRAPPER_ENV_VAR);
@@ -1775,7 +1785,11 @@ session_cache_dir: {}"#,
cmd.arg("-Crelocation-model=pic");
}
tracing::trace!("Direct rustc command: {:#?}", rustc_args.args);
tracing::debug!("Direct rustc: {:#?}", cmd);
cmd.envs(rustc_args.envs.iter().cloned());
tracing::trace!("Setting env vars: {:#?}", rustc_args.envs);
Ok(cmd)
}
@@ -1800,8 +1814,6 @@ session_cache_dir: {}"#,
.args(self.cargo_build_arguments(ctx))
.envs(self.cargo_build_env_vars(ctx)?);
tracing::trace!("Cargo command: {:#?}", cmd);
if ctx.mode == BuildMode::Fat {
cmd.env(
DX_RUSTC_WRAPPER_ENV_VAR,
@@ -1816,6 +1828,8 @@ session_cache_dir: {}"#,
);
}
tracing::debug!("Cargo: {:#?}", cmd);
Ok(cmd)
}
}
@@ -1942,14 +1956,7 @@ session_cache_dir: {}"#,
//
// https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md
//
// The trickiest one here is -Crelocation-model=pic, which forces data symbols
// into a GOT, making it possible to import them from the main module.
//
// I think we can make relocation-model=pic work for non-wasm platforms, enabling
// fully relocatable modules with no host coordination in lieu of sending out
// the aslr slide at runtime.
//
// The other tricky one is -Ctarget-cpu=mvp, which prevents rustc from generating externref
// The tricky one is -Ctarget-cpu=mvp, which prevents rustc from generating externref
// entries.
//
// https://blog.rust-lang.org/2024/09/24/webassembly-targets-change-in-default-target-features/#disabling-on-by-default-webassembly-proposals
@@ -1958,7 +1965,6 @@ session_cache_dir: {}"#,
if self.platform == Platform::Web
|| self.triple.operating_system == OperatingSystem::Wasi
{
// cargo_args.push("-Crelocation-model=pic".into());
cargo_args.push("-Ctarget-cpu=mvp".into());
cargo_args.push("-Clink-arg=--no-gc-sections".into());
cargo_args.push("-Clink-arg=--growable-table".into());
@@ -2324,12 +2330,19 @@ session_cache_dir: {}"#,
app.join("proguard-rules.pro"),
include_bytes!("../../assets/android/gen/app/proguard-rules.pro"),
)?;
write(
app.join("src").join("main").join("AndroidManifest.xml"),
hbs.render_template(
let manifest_xml = match self.config.application.android_manifest.as_deref() {
Some(manifest) => std::fs::read_to_string(self.package_manifest_dir().join(manifest))
.context("Failed to locate custom AndroidManifest.xml")?,
_ => hbs.render_template(
include_str!("../../assets/android/gen/app/src/main/AndroidManifest.xml.hbs"),
&hbs_data,
)?,
};
write(
app.join("src").join("main").join("AndroidManifest.xml"),
manifest_xml,
)?;
// Write the main activity manually since tao dropped support for it
@@ -2657,8 +2670,6 @@ session_cache_dir: {}"#,
return platforms;
};
tracing::debug!("Default features: {default:?}");
// we only trace features 1 level deep..
for feature in default.iter() {
// If the user directly specified a platform we can just use that.
@@ -2688,8 +2699,6 @@ session_cache_dir: {}"#,
platforms.sort();
platforms.dedup();
tracing::debug!("Default platforms: {platforms:?}");
platforms
}
@@ -2876,13 +2885,15 @@ session_cache_dir: {}"#,
// If pre-compressing is enabled, we can pre_compress the wasm-bindgen output
let pre_compress = self.should_pre_compress_web_assets(self.release);
ctx.status_compressing_assets();
let asset_dir = self.asset_dir();
tokio::task::spawn_blocking(move || {
crate::fastfs::pre_compress_folder(&asset_dir, pre_compress)
})
.await
.unwrap()?;
if pre_compress {
ctx.status_compressing_assets();
let asset_dir = self.asset_dir();
tokio::task::spawn_blocking(move || {
crate::fastfs::pre_compress_folder(&asset_dir, pre_compress)
})
.await
.unwrap()?;
}
}
Platform::MacOS => {}
Platform::Windows => {}
@@ -3132,6 +3143,22 @@ session_cache_dir: {}"#,
pub executable_name: String,
}
// Attempt to use the user's manually specified
let _app = &self.config.application;
match platform {
Platform::MacOS => {
if let Some(macos_info_plist) = _app.macos_info_plist.as_deref() {
return Ok(std::fs::read_to_string(macos_info_plist)?);
}
}
Platform::Ios => {
if let Some(macos_info_plist) = _app.ios_info_plist.as_deref() {
return Ok(std::fs::read_to_string(macos_info_plist)?);
}
}
_ => {}
}
match platform {
Platform::MacOS => handlebars::Handlebars::new()
.render_template(
@@ -3273,9 +3300,15 @@ session_cache_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());
tracing::debug!(
r#"Initialized build dirs:
• root dir: {:?}
• exe dir: {:?}
• asset dir: {:?}"#,
self.root_dir(),
self.exe_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
@@ -3387,7 +3420,6 @@ session_cache_dir: {}"#,
/// This should generally be only called on the first build since it takes time to verify the tooling
/// is in place, and we don't want to slow down subsequent builds.
pub(crate) async fn verify_tooling(&self, ctx: &BuildContext) -> Result<()> {
tracing::debug!("Verifying tooling...");
ctx.status_installing_tooling();
self
@@ -3768,4 +3800,116 @@ r#" <script>
.to_path_buf()
.into()
}
pub(crate) async fn start_simulators(&self) -> Result<()> {
if self.device {
return Ok(());
}
match self.platform {
// Boot an iOS simulator if one is not already running.
//
// We always choose the most recently opened simulator based on the xcrun list.
// Note that simulators can be running but the simulator app itself is not open.
// Calling `open::that` is always fine, even on running apps, since apps are singletons.
Platform::Ios => {
#[derive(Deserialize, Debug)]
struct XcrunListJson {
// "com.apple.CoreSimulator.SimRuntime.iOS-18-4": [{}, {}, {}]
devices: BTreeMap<String, Vec<XcrunDevice>>,
}
#[derive(Deserialize, Debug)]
struct XcrunDevice {
#[serde(rename = "lastBootedAt")]
last_booted_at: Option<String>,
udid: String,
name: String,
state: String,
}
let xcrun_list = Command::new("xcrun")
.arg("simctl")
.arg("list")
.arg("-j")
.output()
.await?;
let as_str = String::from_utf8_lossy(&xcrun_list.stdout);
let xcrun_list_json = serde_json::from_str::<XcrunListJson>(as_str.trim());
if let Ok(xcrun_list_json) = xcrun_list_json {
if xcrun_list_json.devices.is_empty() {
tracing::warn!(
"No iOS sdks installed found. Please install the iOS SDK in Xcode."
);
}
if let Some((_rt, devices)) = xcrun_list_json.devices.iter().next() {
if devices.iter().all(|device| device.state != "Booted") {
let last_booted =
devices
.iter()
.max_by_key(|device| match device.last_booted_at {
Some(ref last_booted) => last_booted,
None => "2000-01-01T01:01:01Z",
});
if let Some(device) = last_booted {
tracing::info!("Booting iOS simulator: \"{}\"", device.name);
Command::new("xcrun")
.arg("simctl")
.arg("boot")
.arg(&device.udid)
.output()
.await?;
}
}
}
}
let path_to_xcode = Command::new("xcode-select")
.arg("--print-path")
.output()
.await?;
let path_to_xcode: PathBuf = String::from_utf8_lossy(&path_to_xcode.stdout)
.as_ref()
.trim()
.into();
let path_to_sim = path_to_xcode.join("Applications").join("Simulator.app");
open::that(path_to_sim)?;
}
Platform::Android => {
let tools = self.workspace.android_tools()?;
tokio::spawn(async move {
let emulator = tools.emulator();
let avds = Command::new(&emulator)
.arg("-list-avds")
.output()
.await
.unwrap();
let avds = String::from_utf8_lossy(&avds.stdout);
let avd = avds.trim().lines().next().map(|s| s.trim().to_string());
if let Some(avd) = avd {
tracing::info!("Booting Android emulator: \"{avd}\"");
Command::new(&emulator)
.arg("-avd")
.arg(avd)
.args(["-netdelay", "none", "-netspeed", "full"])
.stdout(std::process::Stdio::null()) // prevent accumulating huge amounts of mem usage
.stderr(std::process::Stdio::null()) // prevent accumulating huge amounts of mem usage
.output()
.await
.unwrap();
} else {
tracing::warn!("No Android emulators found. Please create one using `emulator -avd <name>`");
}
});
}
_ => {
// nothing - maybe on the web we should open the browser?
}
};
Ok(())
}
}
+19 -3
View File
@@ -11,6 +11,7 @@ use tokio::process::Command;
/// https://gist.github.com/Pulimet/5013acf2cd5b28e55036c82c91bd56d8?permalink_comment_id=3678614
#[derive(Debug, Clone)]
pub(crate) struct AndroidTools {
pub(crate) sdk: Option<PathBuf>,
pub(crate) ndk: PathBuf,
pub(crate) adb: PathBuf,
pub(crate) java_home: Option<PathBuf>,
@@ -114,6 +115,7 @@ pub fn get_android_tools() -> Option<Arc<AndroidTools>> {
ndk,
adb,
java_home,
sdk,
}))
}
@@ -168,6 +170,18 @@ impl AndroidTools {
))
}
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/
self.sdk
.clone()
.unwrap_or_else(|| self.ndk.parent().unwrap().parent().unwrap().to_path_buf())
}
pub(crate) fn emulator(&self) -> PathBuf {
self.sdk().join("emulator").join("emulator")
}
// todo(jon): this should be configurable
pub(crate) fn min_sdk_version(&self) -> u32 {
24
@@ -230,8 +244,11 @@ impl AndroidTools {
"x86_64" => {
triple.architecture = Architecture::X86_64;
}
"" => {
tracing::debug!("No device running - probably waiting for emulator");
}
other => {
tracing::warn!("Unknown architecture from adb: {other}");
tracing::debug!("Unknown architecture from adb: {other}");
}
},
Ok(Err(err)) => {
@@ -248,10 +265,9 @@ impl AndroidTools {
fn var_or_debug(name: &str) -> Option<PathBuf> {
use std::env::var;
use tracing::debug;
var(name)
.inspect_err(|_| debug!("{name} not set"))
.inspect_err(|_| tracing::trace!("{name} not set"))
.ok()
.map(PathBuf::from)
}