Pārlūkot izejas kodu

feat: device hot-reload, tools-relative adb, auto port bind (#3586)

* wip: bind to 0.0.0.0 instead of 127.0.0.1

* dont hang in background - ignore suspend commands when tty is disabled

* use system adb

* pass configs through to android mobile launch

* fix: line sizes + clear pushes frame

* dont resume window state on mobile

* fix screen sizing for ipad

* attempt and fail to implement codesigning

* Clippy and note about safety in android bindings
Jonathan Kelley 5 mēneši atpakaļ
vecāks
revīzija
8334f7fbaa

+ 43 - 4
Cargo.lock

@@ -3509,6 +3509,7 @@ dependencies = [
  "include_dir",
  "itertools 0.13.0",
  "krates",
+ "local-ip-address",
  "log",
  "manganis-core",
  "notify",
@@ -3516,6 +3517,7 @@ dependencies = [
  "once_cell",
  "open",
  "path-absolutize",
+ "plist",
  "prettyplease",
  "proc-macro2",
  "ratatui",
@@ -6298,9 +6300,9 @@ dependencies = [
 
 [[package]]
 name = "hstr"
-version = "0.2.15"
+version = "0.2.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63d6824358c0fd9a68bb23999ed2ef76c84f79408a26ef7ae53d5f370c94ad36"
+checksum = "dae404c0c5d4e95d4858876ab02eecd6a196bb8caa42050dfa809938833fc412"
 dependencies = [
  "hashbrown 0.14.5",
  "new_debug_unreachable",
@@ -7691,6 +7693,18 @@ version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
 
+[[package]]
+name = "local-ip-address"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3669cf5561f8d27e8fc84cc15e58350e70f557d4d65f70e3154e54cd2f8e1782"
+dependencies = [
+ "libc",
+ "neli",
+ "thiserror 1.0.69",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "lock_api"
 version = "0.4.12"
@@ -8203,6 +8217,31 @@ dependencies = [
  "jni-sys",
 ]
 
+[[package]]
+name = "neli"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1100229e06604150b3becd61a4965d5c70f3be1759544ea7274166f4be41ef43"
+dependencies = [
+ "byteorder",
+ "libc",
+ "log",
+ "neli-proc-macros",
+]
+
+[[package]]
+name = "neli-proc-macros"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c168194d373b1e134786274020dae7fc5513d565ea2ebb9bc9ff17ffb69106d4"
+dependencies = [
+ "either",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "nested-suspense"
 version = "0.1.0"
@@ -11839,9 +11878,9 @@ dependencies = [
 
 [[package]]
 name = "static-self"
-version = "0.1.1"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "253e76c8c993a7b1b201b0539228b334582153cd4364292822d2c30776d469c7"
+checksum = "f6635404b73efc136af3a7956e53c53d4f34b2f16c95a15c438929add0f69412"
 dependencies = [
  "smallvec",
  "static-self-derive",

+ 2 - 7
packages/cli-config/src/lib.rs

@@ -99,15 +99,10 @@ macro_rules! read_env_config {
 /// For reference, the devserver typically lives on `127.0.0.1:8080` and serves the devserver websocket
 /// on `127.0.0.1:8080/_dioxus`.
 pub fn devserver_raw_addr() -> Option<SocketAddr> {
-    // On android, 10.0.2.2 is the default loopback
-    if cfg!(target_os = "android") {
-        return Some("10.0.2.2:8080".parse().unwrap());
-    }
-
     std::env::var(DEVSERVER_RAW_ADDR_ENV)
-        .map(|s| s.parse().ok())
+        .unwrap_or_else(|_| "127.0.0.1:8080".to_string())
+        .parse()
         .ok()
-        .flatten()
 }
 
 /// Get the address of the devserver for use over a websocket

+ 2 - 0
packages/cli/Cargo.toml

@@ -122,7 +122,9 @@ tauri-bundler = { workspace = true }
 include_dir = "0.7.4"
 flate2 = "1.0.35"
 tar = "0.4.43"
+local-ip-address = "0.6.3"
 dircpy = "0.3.19"
+plist = "1.7.0"
 
 [build-dependencies]
 built = { version = "=0.7.4", features = ["git2"] }

+ 50 - 18
packages/cli/assets/ios/ios.plist.hbs

@@ -3,27 +3,59 @@
 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-  <key>CFBundleDisplayName</key>
-  <string>{{ display_name }}</string>
+	<key>CFBundleDisplayName</key>
+	<string>{{ display_name }}</string>
 
-  <key>CFBundleExecutable</key>
-  <string>{{ executable_name }}</string>
+	<key>CFBundleExecutable</key>
+	<string>{{ executable_name }}</string>
 
-  <key>CFBundleIdentifier</key>
-  <string>{{ bundle_identifier }}</string>
+	<key>CFBundleIdentifier</key>
+	<string>{{ bundle_identifier }}</string>
 
-  <key>CFBundleName</key>
-  <string>{{ bundle_name }}</string>
+	<key>CFBundleName</key>
+	<string>{{ bundle_name }}</string>
 
-  <key>CFBundleVersion</key>
-  <string>0.1.0</string>
-  <key>CFBundleShortVersionString</key>
-  <string>0.1.0</string>
-  <key>CFBundleDevelopmentRegion</key>
-  <string>en_US</string>
-  <key>UILaunchStoryboardName</key>
-  <string></string>
-  <key>LSRequiresIPhoneOS</key>
-  <true/>
+	<key>CFBundleVersion</key>
+	<string>0.1.0</string>
+	<key>CFBundleShortVersionString</key>
+	<string>0.1.0</string>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en_US</string>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>UISupportsTrueScreenSizeOnMac</key>
+	<true/>
+	<key>UIRequiredDeviceCapabilities</key>
+	<array>
+		<string>arm64</string>
+		<string>metal</string>
+	</array>
+	<key>UIDeviceFamily</key>
+	<array>
+		<integer>1</integer>
+		<integer>2</integer>
+	</array>
+	<key>CFBundleSupportedPlatforms</key>
+	<array>
+		<string>iPhoneOS</string>
+		<string>iPadOS</string>
+	</array>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UISupportedInterfaceOrientations~ipad</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
 </dict>
 </plist>

+ 1 - 1
packages/cli/src/cli/build.rs

@@ -156,7 +156,7 @@ impl BuildArgs {
         if self.platform == Some(Platform::Android) && self.target_args.arch.is_none() {
             tracing::debug!("No android arch provided, attempting to auto detect.");
 
-            let arch = Arch::autodetect().await;
+            let arch = DioxusCrate::autodetect_android_arch().await;
 
             // Some extra logs
             let arch = match arch {

+ 2 - 2
packages/cli/src/cli/serve.rs

@@ -96,8 +96,8 @@ impl ServeArgs {
     }
 
     pub(crate) fn is_interactive_tty(&self) -> bool {
-        use crossterm::tty::IsTty;
-        std::io::stdout().is_tty() && self.interactive.unwrap_or(true)
+        use std::io::IsTerminal;
+        std::io::stdout().is_terminal() && self.interactive.unwrap_or(true)
     }
 
     pub(crate) fn should_proxy_build(&self) -> bool {

+ 0 - 44
packages/cli/src/cli/target.rs

@@ -1,7 +1,5 @@
 use super::*;
-use once_cell::sync::OnceCell;
 use std::path::Path;
-use tokio::process::Command;
 
 /// Information about the target to build
 #[derive(Clone, Debug, Default, Deserialize, Parser)]
@@ -73,48 +71,6 @@ pub(crate) enum Arch {
 }
 
 impl Arch {
-    pub(crate) async fn autodetect() -> Option<Self> {
-        // Try auto detecting arch through adb.
-        static AUTO_ARCH: OnceCell<Option<Arch>> = OnceCell::new();
-
-        match AUTO_ARCH.get() {
-            Some(a) => *a,
-            None => {
-                // TODO: Wire this up with --device flag. (add `-s serial`` flag before `shell` arg)
-                let output = Command::new("adb")
-                    .arg("shell")
-                    .arg("uname")
-                    .arg("-m")
-                    .output()
-                    .await;
-
-                let out = match output {
-                    Ok(o) => o,
-                    Err(e) => {
-                        tracing::debug!("ADB command failed: {:?}", e);
-                        return None;
-                    }
-                };
-
-                // Parse ADB output
-                let Ok(out) = String::from_utf8(out.stdout) else {
-                    tracing::debug!("ADB returned unexpected data.");
-                    return None;
-                };
-                let trimmed = out.trim().to_string();
-                tracing::trace!("ADB Returned: `{trimmed:?}`");
-
-                // Set the cell
-                let arch = Arch::try_from(trimmed).ok();
-                AUTO_ARCH
-                    .set(arch)
-                    .expect("the cell should have been checked empty by the match condition");
-
-                arch
-            }
-        }
-    }
-
     pub(crate) fn android_target_triplet(&self) -> &'static str {
         match self {
             Arch::Arm => "armv7-linux-androideabi",

+ 4 - 22
packages/cli/src/config/serve.rs

@@ -4,31 +4,13 @@ use clap::Parser;
 use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
 
 /// The arguments for the address the server will run on
-#[derive(Clone, Debug, Parser)]
+#[derive(Clone, Debug, Default, Parser)]
 pub(crate) struct AddressArguments {
     /// The port the server will run on
     #[clap(long)]
-    #[clap(default_value_t = default_port())]
-    pub(crate) port: u16,
+    pub(crate) port: Option<u16>,
 
     /// The address the server will run on
-    #[clap(long, default_value_t = default_address())]
-    pub(crate) addr: std::net::IpAddr,
-}
-
-impl Default for AddressArguments {
-    fn default() -> Self {
-        Self {
-            port: default_port(),
-            addr: default_address(),
-        }
-    }
-}
-
-fn default_port() -> u16 {
-    8080
-}
-
-fn default_address() -> IpAddr {
-    IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1))
+    #[clap(long)]
+    pub(crate) addr: Option<std::net::IpAddr>,
 }

+ 84 - 11
packages/cli/src/dioxus_crate.rs

@@ -1,13 +1,15 @@
-use crate::CliSettings;
 use crate::{config::DioxusConfig, TargetArgs};
+use crate::{Arch, CliSettings};
 use crate::{Platform, Result};
 use anyhow::Context;
 use itertools::Itertools;
 use krates::{cm::Target, KrateDetails};
 use krates::{cm::TargetKind, Cmd, Krates, NodeId};
+use once_cell::sync::OnceCell;
 use std::path::Path;
 use std::path::PathBuf;
 use std::sync::Arc;
+use tokio::process::Command;
 use toml_edit::Item;
 
 // Contains information about the crate we are currently in and the dioxus config for that crate
@@ -616,19 +618,38 @@ impl DioxusCrate {
         krates
     }
 
+    /// Attempt to retrieve the path to ADB
+    pub(crate) fn android_adb() -> PathBuf {
+        static PATH: once_cell::sync::Lazy<PathBuf> = once_cell::sync::Lazy::new(|| {
+            let Some(sdk) = DioxusCrate::android_sdk() else {
+                return PathBuf::from("adb");
+            };
+
+            let tools = sdk.join("platform-tools");
+
+            if tools.join("adb").exists() {
+                return tools.join("adb");
+            }
+
+            if tools.join("adb.exe").exists() {
+                return tools.join("adb.exe");
+            }
+
+            PathBuf::from("adb")
+        });
+
+        PATH.clone()
+    }
+
+    pub(crate) fn android_sdk() -> Option<PathBuf> {
+        var_or_debug("ANDROID_SDK_ROOT")
+            .or_else(|| var_or_debug("ANDROID_SDK"))
+            .or_else(|| var_or_debug("ANDROID_HOME"))
+    }
+
     pub(crate) fn android_ndk(&self) -> Option<PathBuf> {
         // "/Users/jonkelley/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang"
         static PATH: once_cell::sync::Lazy<Option<PathBuf>> = once_cell::sync::Lazy::new(|| {
-            use std::env::var;
-            use tracing::debug;
-
-            fn var_or_debug(name: &str) -> Option<PathBuf> {
-                var(name)
-                    .inspect_err(|_| debug!("{name} not set"))
-                    .ok()
-                    .map(PathBuf::from)
-            }
-
             // attempt to autodetect the ndk path from env vars (usually set by the shell)
             let auto_detected_ndk =
                 var_or_debug("NDK_HOME").or_else(|| var_or_debug("ANDROID_NDK_HOME"));
@@ -655,6 +676,48 @@ impl DioxusCrate {
         PATH.clone()
     }
 
+    pub(crate) async fn autodetect_android_arch() -> Option<Arch> {
+        // Try auto detecting arch through adb.
+        static AUTO_ARCH: OnceCell<Option<Arch>> = OnceCell::new();
+
+        match AUTO_ARCH.get() {
+            Some(a) => *a,
+            None => {
+                // TODO: Wire this up with --device flag. (add `-s serial`` flag before `shell` arg)
+                let output = Command::new("adb")
+                    .arg("shell")
+                    .arg("uname")
+                    .arg("-m")
+                    .output()
+                    .await;
+
+                let out = match output {
+                    Ok(o) => o,
+                    Err(e) => {
+                        tracing::debug!("ADB command failed: {:?}", e);
+                        return None;
+                    }
+                };
+
+                // Parse ADB output
+                let Ok(out) = String::from_utf8(out.stdout) else {
+                    tracing::debug!("ADB returned unexpected data.");
+                    return None;
+                };
+                let trimmed = out.trim().to_string();
+                tracing::trace!("ADB Returned: `{trimmed:?}`");
+
+                // Set the cell
+                let arch = Arch::try_from(trimmed).ok();
+                AUTO_ARCH
+                    .set(arch)
+                    .expect("the cell should have been checked empty by the match condition");
+
+                arch
+            }
+        }
+    }
+
     pub(crate) fn mobile_org(&self) -> String {
         let identifier = self.bundle_identifier();
         let mut split = identifier.splitn(3, '.');
@@ -763,3 +826,13 @@ fn find_main_package(krates: &Krates, package: Option<String>) -> Result<NodeId>
     let package = krates.nid_for_kid(kid).unwrap();
     Ok(package)
 }
+
+fn var_or_debug(name: &str) -> Option<PathBuf> {
+    use std::env::var;
+    use tracing::debug;
+
+    var(name)
+        .inspect_err(|_| debug!("{name} not set"))
+        .ok()
+        .map(PathBuf::from)
+}

+ 1 - 0
packages/cli/src/logging.rs

@@ -135,6 +135,7 @@ impl TraceController {
         let (tui_tx, tui_rx) = unbounded();
         TUI_ACTIVE.store(true, Ordering::Relaxed);
         TUI_TX.set(tui_tx.clone()).unwrap();
+
         Self { tui_rx }
     }
 

+ 15 - 6
packages/cli/src/rustc.rs

@@ -6,21 +6,30 @@ use tokio::process::Command;
 #[derive(Debug, Default)]
 pub struct RustcDetails {
     pub sysroot: PathBuf,
+    pub version: String,
 }
 
 impl RustcDetails {
     /// Find the current sysroot location using the CLI
     pub async fn from_cli() -> Result<RustcDetails> {
-        let output = Command::new("rustc")
+        let sysroot = Command::new("rustc")
             .args(["--print", "sysroot"])
             .output()
-            .await?;
+            .await
+            .map(|out| String::from_utf8(out.stdout))?
+            .context("Failed to extract rustc sysroot output")?;
 
-        let stdout =
-            String::from_utf8(output.stdout).context("Failed to extract rustc sysroot output")?;
+        let rustc_version = Command::new("rustc")
+            .args(["--version"])
+            .output()
+            .await
+            .map(|out| String::from_utf8(out.stdout))?
+            .context("Failed to extract rustc version output")?;
 
-        let sysroot = PathBuf::from(stdout.trim());
-        Ok(Self { sysroot })
+        Ok(Self {
+            sysroot: sysroot.trim().into(),
+            version: rustc_version.trim().into(),
+        })
     }
 
     pub fn has_wasm32_unknown_unknown(&self) -> bool {

+ 5 - 2
packages/cli/src/serve/ansi_buffer.rs

@@ -33,14 +33,17 @@ impl AnsiStringLine {
     fn trim_end(&mut self) {
         for y in 0..self.buf.area.height {
             let start_x = self.buf.area.width;
-            let mut first_non_empty = start_x - 1;
+            let mut first_non_empty = start_x;
             for x in (0..start_x).rev() {
                 if self.buf.get(x, y) != &buffer::Cell::EMPTY {
                     break;
                 }
                 first_non_empty = x;
             }
-            self.buf.get_mut(first_non_empty, y).set_symbol(SENTINEL);
+
+            if first_non_empty != start_x {
+                self.buf.get_mut(first_non_empty, y).set_symbol(SENTINEL);
+            }
         }
     }
 

+ 150 - 29
packages/cli/src/serve/handle.rs

@@ -1,4 +1,4 @@
-use crate::{AppBundle, Platform, Result};
+use crate::{AppBundle, DioxusCrate, Platform, Result};
 use anyhow::Context;
 use dioxus_cli_opt::process_file_to;
 use std::{
@@ -210,7 +210,7 @@ impl AppHandle {
             if let Some(bundled_name) = bundled_name.as_ref() {
                 let target = format!("/data/local/tmp/dx/{}", bundled_name.display());
                 tracing::debug!("Pushing asset to device: {target}");
-                let res = tokio::process::Command::new("adb")
+                let res = tokio::process::Command::new(DioxusCrate::android_adb())
                     .arg("push")
                     .arg(&changed_file)
                     .arg(target)
@@ -323,31 +323,6 @@ impl AppHandle {
     /// better support for codesigning and entitlements.
     #[allow(unused)]
     async fn open_ios_device(&self) -> Result<()> {
-        // APP_PATH="target/aarch64-apple-ios/debug/bundle/ios/DioxusApp.app"
-
-        // # get the device id by jq-ing the json of the device list
-        // xcrun devicectl list devices --json-output target/deviceid.json
-        // DEVICE_UUID=$(jq -r '.result.devices[0].identifier' target/deviceid.json)
-
-        // xcrun devicectl device install app --device "${DEVICE_UUID}" "${APP_PATH}" --json-output target/xcrun.json
-
-        // # get the installation url by jq-ing the json of the device install
-        // INSTALLATION_URL=$(jq -r '.result.installedApplications[0].installationURL' target/xcrun.json)
-
-        // # launch the app
-        // # todo: we can just background it immediately and then pick it up for loading its logs
-        // xcrun devicectl device process launch --device "${DEVICE_UUID}" "${INSTALLATION_URL}"
-
-        // # # launch the app and put it in background
-        // # xcrun devicectl device process launch --no-activate --verbose --device "${DEVICE_UUID}" "${INSTALLATION_URL}" --json-output "${XCRUN_DEVICE_PROCESS_LAUNCH_LOG_DIR}"
-
-        // # # Extract background PID of status app
-        // # STATUS_PID=$(jq -r '.result.process.processIdentifier' "${XCRUN_DEVICE_PROCESS_LAUNCH_LOG_DIR}")
-        // # "${GIT_ROOT}/scripts/wait-for-metro-port.sh"  2>&1
-
-        // # # now that metro is ready, resume the app from background
-        // # xcrun devicectl device process resume --device "${DEVICE_UUID}" --pid "${STATUS_PID}" > "${XCRUN_DEVICE_PROCESS_RESUME_LOG_DIR}" 2>&1
-
         use serde_json::Value;
         let app_path = self.app.build.root_dir();
 
@@ -403,6 +378,7 @@ impl AppHandle {
         }
 
         async fn get_installation_url(device_uuid: &str, app_path: &Path) -> Result<String> {
+            // xcrun devicectl device install app --device <uuid> --path <path> --json-output
             let output = Command::new("xcrun")
                 .args([
                     "devicectl",
@@ -489,6 +465,151 @@ impl AppHandle {
         unimplemented!("dioxus-cli doesn't support ios devices yet.")
     }
 
+    #[allow(unused)]
+    async fn codesign_ios(&self) -> Result<()> {
+        const CODESIGN_ERROR: &str = r#"This is likely because you haven't
+- Created a provisioning profile before
+- Accepted the Apple Developer Program License Agreement
+
+The agreement changes frequently and might need to be accepted again.
+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()
+            .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: {}
+
+{CODESIGN_ERROR}
+"#,
+                profiles_folder.display()
+            )
+        }
+
+        let identities = Command::new("security")
+            .args(["find-identity", "-v", "-p", "codesigning"])
+            .output()
+            .await
+            .context("Failed to run `security find-identity -v -p codesigning`")
+            .map(|e| {
+                String::from_utf8(e.stdout)
+                    .context("Failed to parse `security find-identity -v -p codesigning`")
+            })??;
+
+        // Parsing this:
+        // 51ADE4986E0033A5DB1C794E0D1473D74FD6F871 "Apple Development: jkelleyrtp@gmail.com (XYZYZY)"
+        let app_dev_name = regex::Regex::new(r#""Apple Development: (.+)""#)
+            .unwrap()
+            .captures(&identities)
+            .and_then(|caps| caps.get(1))
+            .map(|m| m.as_str())
+            .context(
+                "Failed to find Apple Development in `security find-identity -v -p codesigning`",
+            )?;
+
+        // Acquire the provision file
+        let provision_file = profiles_folder
+            .read_dir()?
+            .flatten()
+            .find(|entry| {
+                entry
+                    .file_name()
+                    .to_str()
+                    .map(|s| s.contains("mobileprovision"))
+                    .unwrap_or_default()
+            })
+            .context("Failed to find a provisioning profile. \n\n{CODESIGN_ERROR}")?;
+
+        // The .mobileprovision file has some random binary thrown into into, but it's still basically a plist
+        // Let's use the plist markers to find the start and end of the plist
+        fn cut_plist(bytes: &[u8], byte_match: &[u8]) -> Option<usize> {
+            bytes
+                .windows(byte_match.len())
+                .enumerate()
+                .rev()
+                .find(|(_, slice)| *slice == byte_match)
+                .map(|(i, _)| i + byte_match.len())
+        }
+        let bytes = std::fs::read(provision_file.path())?;
+        let cut1 = cut_plist(&bytes, b"<plist").context("Failed to parse .mobileprovision file")?;
+        let cut2 = cut_plist(&bytes, r#"</dict>"#.as_bytes())
+            .context("Failed to parse .mobileprovision file")?;
+        let sub_bytes = &bytes[(cut1 - 6)..cut2];
+        let mbfile: ProvisioningProfile =
+            plist::from_bytes(sub_bytes).context("Failed to parse .mobileprovision file")?;
+
+        #[derive(serde::Deserialize, Debug)]
+        struct ProvisioningProfile {
+            #[serde(rename = "TeamIdentifier")]
+            team_identifier: Vec<String>,
+            #[serde(rename = "ApplicationIdentifierPrefix")]
+            application_identifier_prefix: Vec<String>,
+            #[serde(rename = "Entitlements")]
+            entitlements: Entitlements,
+        }
+
+        #[derive(serde::Deserialize, Debug)]
+        struct Entitlements {
+            #[serde(rename = "application-identifier")]
+            application_identifier: String,
+            #[serde(rename = "keychain-access-groups")]
+            keychain_access_groups: Vec<String>,
+        }
+
+        let entielements_xml = format!(
+            r#"
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0"><dict>
+    <key>application-identifier</key>
+    <string>{APPLICATION_IDENTIFIER}</string>
+    <key>keychain-access-groups</key>
+    <array>
+        <string>{APP_ID_ACCESS_GROUP}.*</string>
+    </array>
+    <key>get-task-allow</key>
+    <true/>
+    <key>com.apple.developer.team-identifier</key>
+    <string>{TEAM_IDENTIFIER}</string>
+</dict></plist>
+        "#,
+            APPLICATION_IDENTIFIER = mbfile.entitlements.application_identifier,
+            APP_ID_ACCESS_GROUP = mbfile.entitlements.keychain_access_groups[0],
+            TEAM_IDENTIFIER = mbfile.team_identifier[0],
+        );
+
+        // write to a temp file
+        let temp_file = tempfile::NamedTempFile::new()?;
+        std::fs::write(temp_file.path(), entielements_xml)?;
+
+        // codesign the app
+        let output = Command::new("codesign")
+            .args([
+                "--force",
+                "--entitlements",
+                temp_file.path().to_str().unwrap(),
+                "--sign",
+                app_dev_name,
+            ])
+            .arg(self.app.build.root_dir())
+            .output()
+            .await
+            .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());
+        }
+
+        Ok(())
+    }
+
     async fn open_android_sim(&self, envs: Vec<(&'static str, String)>) {
         let apk_path = self.app.apk_path();
         let full_mobile_app_name = self.app.build.krate.full_mobile_app_name();
@@ -497,7 +618,7 @@ impl AppHandle {
         tokio::task::spawn(async move {
             // Install
             // adb install -r app-debug.apk
-            if let Err(e) = Command::new("adb")
+            if let Err(e) = Command::new(DioxusCrate::android_adb())
                 .arg("install")
                 .arg("-r")
                 .arg(apk_path)
@@ -513,7 +634,7 @@ impl AppHandle {
             // 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")
+            if let Err(e) = Command::new(DioxusCrate::android_adb())
                 .arg("shell")
                 .arg("am")
                 .arg("start")

+ 2 - 2
packages/cli/src/serve/mod.rs

@@ -45,11 +45,11 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
     let krate = args.load_krate().await?;
 
     // Note that starting the builder will queue up a build immediately
+    let mut screen = Output::start(&args).await?;
     let mut builder = Builder::start(&krate, args.build_args())?;
     let mut devserver = WebServer::start(&krate, &args)?;
     let mut watcher = Watcher::start(&krate, &args);
     let mut runner = AppRunner::start(&krate);
-    let mut screen = Output::start(&args)?;
 
     // This is our default splash screen. We might want to make this a fancier splash screen in the future
     // Also, these commands might not be the most important, but it's all we've got enabled right now
@@ -58,7 +58,7 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
                 Serving your Dioxus app: {} 🚀
                 • Press `ctrl+c` to exit the server
                 • Press `r` to rebuild the app
-                • Press `o` to open the app
+                • Press `p` to toggle automatic rebuilds
                 • Press `v` to toggle verbose logging
                 • Press `/` for more commands and shortcuts
                 Learn more at https://dioxuslabs.com/learn/0.6/getting_started

+ 127 - 28
packages/cli/src/serve/output.rs

@@ -1,6 +1,7 @@
 use crate::{
     serve::{ansi_buffer::AnsiStringLine, Builder, ServeUpdate, Watcher, WebServer},
-    BuildStage, BuildUpdate, DioxusCrate, Platform, ServeArgs, TraceContent, TraceMsg, TraceSrc,
+    BuildStage, BuildUpdate, DioxusCrate, Platform, RustcDetails, ServeArgs, TraceContent,
+    TraceMsg, TraceSrc,
 };
 use crossterm::{
     cursor::{Hide, Show},
@@ -28,7 +29,7 @@ use tracing::Level;
 const TICK_RATE_MS: u64 = 100;
 const VIEWPORT_MAX_WIDTH: u16 = 100;
 const VIEWPORT_HEIGHT_SMALL: u16 = 5;
-const VIEWPORT_HEIGHT_BIG: u16 = 12;
+const VIEWPORT_HEIGHT_BIG: u16 = 13;
 
 /// The TUI that drives the console output.
 ///
@@ -63,6 +64,8 @@ pub struct Output {
     // ! needs to be wrapped in an &mut since `render stateful widget` requires &mut... but our
     // "render" method only borrows &self (for no particular reason at all...)
     throbber: RefCell<throbber_widgets_tui::ThrobberState>,
+
+    rustc_details: RustcDetails,
 }
 
 #[allow(unused)]
@@ -76,17 +79,9 @@ struct RenderState<'a> {
 }
 
 impl Output {
-    pub(crate) fn start(cfg: &ServeArgs) -> io::Result<Self> {
+    pub(crate) async fn start(cfg: &ServeArgs) -> crate::Result<Self> {
         let mut output = Self {
-            term: Rc::new(RefCell::new(
-                Terminal::with_options(
-                    CrosstermBackend::new(stdout()),
-                    TerminalOptions {
-                        viewport: Viewport::Inline(VIEWPORT_HEIGHT_SMALL),
-                    },
-                )
-                .ok(),
-            )),
+            term: Rc::new(RefCell::new(None)),
             interactive: cfg.is_interactive_tty(),
             dx_version: format!(
                 "{}-{}",
@@ -95,7 +90,6 @@ impl Output {
             ),
             platform: cfg.build_arguments.platform.expect("To be resolved by now"),
             events: None,
-            // messages: Vec::new(),
             more_modal_open: false,
             pending_logs: VecDeque::new(),
             throbber: RefCell::new(throbber_widgets_tui::ThrobberState::default()),
@@ -107,6 +101,7 @@ impl Output {
                 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
                 interval
             },
+            rustc_details: RustcDetails::from_cli().await?,
         };
 
         output.startup()?;
@@ -127,11 +122,24 @@ impl Output {
                 original_hook(info);
             }));
 
-            enable_raw_mode()?;
-            stdout()
-                .execute(Hide)?
-                .execute(EnableFocusChange)?
-                .execute(EnableBracketedPaste)?;
+            // Check if writing the terminal is going to block infinitely.
+            // If it does, we should disable interactive mode. This ensures we work with programs like `bg`
+            // which suspend the process and cause us to block when writing output.
+            if Self::enable_raw_mode().is_err() {
+                self.term.take();
+                self.interactive = false;
+                return Ok(());
+            }
+
+            self.term.replace(
+                Terminal::with_options(
+                    CrosstermBackend::new(stdout()),
+                    TerminalOptions {
+                        viewport: Viewport::Inline(VIEWPORT_HEIGHT_SMALL),
+                    },
+                )
+                .ok(),
+            );
 
             // Initialize the event stream here - this is optional because an EvenStream in a non-interactive
             // terminal will cause a panic instead of simply doing nothing.
@@ -142,6 +150,36 @@ impl Output {
         Ok(())
     }
 
+    /// Enable raw mode, but don't let it block forever.
+    ///
+    /// This lets us check if writing to tty is going to block forever and then recover, allowing
+    /// interopability with programs like `bg`.
+    fn enable_raw_mode() -> io::Result<()> {
+        #[cfg(unix)]
+        {
+            use tokio::signal::unix::{signal, SignalKind};
+
+            // Ignore SIGTSTP, SIGTTIN, and SIGTTOU
+            _ = signal(SignalKind::from_raw(20))?; // SIGTSTP
+            _ = signal(SignalKind::from_raw(21))?; // SIGTTIN
+            _ = signal(SignalKind::from_raw(22))?; // SIGTTOU
+        }
+
+        use std::io::IsTerminal;
+
+        if !stdout().is_terminal() {
+            return io::Result::Err(io::Error::new(io::ErrorKind::Other, "Not a terminal"));
+        }
+
+        enable_raw_mode()?;
+        stdout()
+            .execute(Hide)?
+            .execute(EnableFocusChange)?
+            .execute(EnableBracketedPaste)?;
+
+        Ok(())
+    }
+
     /// Call the shutdown functions that might mess with the terminal settings - see the related code
     /// in "startup" for more details about what we need to unset
     pub(crate) fn shutdown(&self) -> io::Result<()> {
@@ -163,6 +201,10 @@ impl Output {
         use futures_util::future::OptionFuture;
         use futures_util::StreamExt;
 
+        if !self.interactive {
+            return std::future::pending().await;
+        }
+
         // Wait for the next user event or animation tick
         loop {
             let next = OptionFuture::from(self.events.as_mut().map(|f| f.next()));
@@ -226,7 +268,16 @@ impl Output {
                 stdout()
                     .execute(Clear(ClearType::All))?
                     .execute(Clear(ClearType::Purge))?;
-                _ = self.term.borrow_mut().as_mut().map(|t| t.clear());
+
+                // Clear the terminal and push the frame to the bottom
+                _ = self.term.borrow_mut().as_mut().map(|t| {
+                    let frame_rect = t.get_frame().area();
+                    let term_size = t.size().unwrap();
+                    let remaining_space = term_size
+                        .height
+                        .saturating_sub(frame_rect.y + frame_rect.height);
+                    t.insert_before(remaining_space, |_| {})
+                });
             }
 
             // Toggle the more modal by swapping the the terminal with a new one
@@ -611,7 +662,7 @@ impl Output {
         self.render_feature_list(frame, app_features, state);
 
         // todo(jon) should we write https ?
-        let address = match state.server.server_address() {
+        let address = match state.server.displayed_address() {
             Some(address) => format!("http://{}", address).blue(),
             None => "no server address".dark_gray(),
         };
@@ -656,16 +707,20 @@ impl Output {
         );
     }
 
-    fn render_more_modal(&self, frame: &mut Frame<'_>, area: Rect, _state: RenderState) {
+    fn render_more_modal(&self, frame: &mut Frame<'_>, area: Rect, state: RenderState) {
+        let [col1, col2] =
+            Layout::horizontal([Constraint::Length(50), Constraint::Fill(1)]).areas(area);
+
         let [top, bottom] = Layout::vertical([Constraint::Fill(1), Constraint::Length(2)])
             .horizontal_margin(1)
-            .areas(area);
+            .areas(col1);
 
-        let meta_list: [_; 5] = Layout::vertical([
+        let meta_list: [_; 6] = Layout::vertical([
             Constraint::Length(1), // spacing
             Constraint::Length(1), // item 1
             Constraint::Length(1), // item 2
             Constraint::Length(1), // item 3
+            Constraint::Length(1), // item 4
             Constraint::Length(1), // Spacing
         ])
         .areas(top);
@@ -680,15 +735,27 @@ impl Output {
         frame.render_widget(
             Paragraph::new(Line::from(vec![
                 "rustc: ".gray(),
-                "1.79.9 (nightly)".yellow(),
+                self.rustc_details.version.as_str().yellow(),
             ])),
             meta_list[2],
         );
         frame.render_widget(
-            Paragraph::new(Line::from(vec!["Hotreload: ".gray(), "rsx only".yellow()])),
+            Paragraph::new(Line::from(vec![
+                "Hotreload: ".gray(),
+                "rsx and assets".yellow(),
+            ])),
             meta_list[3],
         );
 
+        let server_address = match state.server.server_address() {
+            Some(address) => format!("http://{}", address).yellow(),
+            None => "no address".dark_gray(),
+        };
+        frame.render_widget(
+            Paragraph::new(Line::from(vec!["Network: ".gray(), server_address])),
+            meta_list[4],
+        );
+
         let links_list: [_; 2] =
             Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(bottom);
 
@@ -707,6 +774,35 @@ impl Output {
             ])),
             links_list[1],
         );
+
+        let cmds = [
+            "",
+            "r: rebuild the app",
+            "o: open the app",
+            "p: pause rebuilds",
+            "v: toggle verbose logs",
+            "t: toggle tracing logs ",
+            "c: clear the screen",
+            "/: toggle more commands",
+        ];
+        let layout: [_; 8] = Layout::vertical(cmds.iter().map(|_| Constraint::Length(1)))
+            .horizontal_margin(1)
+            .areas(col2);
+        for (idx, cmd) in cmds.iter().enumerate() {
+            if cmd.is_empty() {
+                continue;
+            }
+
+            let (cmd, detail) = cmd.split_once(": ").unwrap_or((cmd, ""));
+            frame.render_widget(
+                Paragraph::new(Line::from(vec![
+                    cmd.gray(),
+                    ": ".gray(),
+                    detail.dark_gray(),
+                ])),
+                layout[idx],
+            );
+        }
     }
 
     /// Render borders around the terminal, forcing an inner clear while we're at it
@@ -905,9 +1001,12 @@ impl Output {
                     line = line.dark_gray();
                 }
 
-                let line_length: usize = line.spans.iter().map(|f| f.content.len()).sum();
-
-                lines.push(AnsiStringLine::new(line_length.max(100) as _).render(&line));
+                // Create the ansi -> raw string line with a width of either the viewport width or the max width
+                let line_length = line.styled_graphemes(Style::default()).count();
+                lines.push(
+                    AnsiStringLine::new(line_length.max(VIEWPORT_MAX_WIDTH.into()) as _)
+                        .render(&line),
+                );
             }
         }
 

+ 9 - 1
packages/cli/src/serve/runner.rs

@@ -194,12 +194,15 @@ impl AppRunner {
 
     /// Open an existing app bundle, if it exists
     pub(crate) async fn open_existing(&mut self, devserver: &WebServer) -> Result<()> {
+        let fullstack_address = devserver.proxied_server_address();
+
         if let Some((_, app)) = self
             .running
             .iter_mut()
             .find(|(platform, _)| **platform != Platform::Server)
         {
-            app.open(devserver.devserver_address(), None, true).await?;
+            app.open(devserver.devserver_address(), fullstack_address, true)
+                .await?;
         }
         Ok(())
     }
@@ -228,6 +231,11 @@ impl AppRunner {
                 continue;
             }
 
+            // Special-case the Cargo.toml file - we want updates here to cause a full rebuild
+            if path.file_name().and_then(|v| v.to_str()) == Some("Cargo.toml") {
+                return None;
+            }
+
             // Otherwise, it might be an asset and we should look for it in all the running apps
             for runner in self.running.values() {
                 if let Some(bundled_name) = runner.hotreload_bundled_asset(&path).await {

+ 61 - 21
packages/cli/src/serve/server.rs

@@ -31,10 +31,9 @@ use serde::{Deserialize, Serialize};
 use std::{
     convert::Infallible,
     fs, io,
-    net::{IpAddr, SocketAddr, TcpListener},
+    net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener},
     path::Path,
-    sync::Arc,
-    sync::RwLock,
+    sync::{Arc, RwLock},
 };
 use tower_http::{
     cors::Any,
@@ -49,7 +48,8 @@ use tower_http::{
 /// which carries all the message types. This would make it easier for us to add more message types
 /// and better tooling on the pages that we serve.
 pub(crate) struct WebServer {
-    devserver_ip: IpAddr,
+    devserver_exposed_ip: IpAddr,
+    devserver_bind_ip: IpAddr,
     devserver_port: u16,
     proxied_port: Option<u16>,
     hot_reload_sockets: Vec<WebSocket>,
@@ -71,18 +71,43 @@ impl WebServer {
         let (hot_reload_sockets_tx, hot_reload_sockets_rx) = futures_channel::mpsc::unbounded();
         let (build_status_sockets_tx, build_status_sockets_rx) = futures_channel::mpsc::unbounded();
 
-        let devserver_ip = args.address.addr;
-        let devserver_port = args.address.port;
-        let devserver_address = SocketAddr::new(devserver_ip, devserver_port);
+        const SELF_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
+
+        // Use 0.0.0.0 as the default address if none is specified - this will let us expose the
+        // devserver to the network (for other devices like phones/embedded)
+        let devserver_bind_ip = args.address.addr.unwrap_or(SELF_IP);
+
+        // If the user specified a port, use that, otherwise use any available port, preferring 8080
+        let devserver_port = args
+            .address
+            .port
+            .unwrap_or_else(|| get_available_port(devserver_bind_ip, Some(8080)).unwrap_or(8080));
 
         // All servers will end up behind us (the devserver) but on a different port
         // This is so we can serve a loading screen as well as devtools without anything particularly fancy
         let proxied_port = args
             .should_proxy_build()
-            .then(|| get_available_port(devserver_ip))
+            .then(|| get_available_port(devserver_bind_ip, None))
             .flatten();
 
-        let proxied_address = proxied_port.map(|port| SocketAddr::new(devserver_ip, port));
+        // Create the listener that we'll pass into the devserver, but save its IP here so
+        // we can display it to the user in the tui
+        let devserver_bind_address = SocketAddr::new(devserver_bind_ip, devserver_port);
+        let listener = std::net::TcpListener::bind(devserver_bind_address).with_context(|| {
+            anyhow::anyhow!(
+                "Failed to bind server to: {devserver_bind_address}, is there another devserver running?\nTo run multiple devservers, use the --port flag to specify a different port"
+            )
+        })?;
+
+        // If the IP is 0.0.0.0, we need to get the actual IP of the machine
+        // This will let ios/android/network clients connect to the devserver
+        let devserver_exposed_ip = if devserver_bind_ip == SELF_IP {
+            local_ip_address::local_ip().unwrap_or(devserver_bind_ip)
+        } else {
+            devserver_bind_ip
+        };
+
+        let proxied_address = proxied_port.map(|port| SocketAddr::new(devserver_exposed_ip, port));
 
         // Set up the router with some shared state that we'll update later to reflect the current state of the build
         let build_status = SharedStatus::new_with_starting_build();
@@ -95,14 +120,6 @@ impl WebServer {
             build_status.clone(),
         )?;
 
-        // Create the listener that we'll pass into the devserver, but save its IP here so
-        // we can display it to the user in the tui
-        let listener = std::net::TcpListener::bind(devserver_address).with_context(|| {
-            anyhow::anyhow!(
-                "Failed to bind server to: {devserver_address}, is there another devserver running?\nTo run multiple devservers, use the --port flag to specify a different port"
-            )
-        })?;
-
         // And finally, start the server mainloop
         tokio::spawn(devserver_mainloop(
             krate.config.web.https.clone(),
@@ -113,7 +130,8 @@ impl WebServer {
         Ok(Self {
             build_status,
             proxied_port,
-            devserver_ip,
+            devserver_bind_ip,
+            devserver_exposed_ip,
             devserver_port,
             hot_reload_sockets: Default::default(),
             build_status_sockets: Default::default(),
@@ -315,13 +333,13 @@ impl WebServer {
 
     /// Get the address the devserver should run on
     pub fn devserver_address(&self) -> SocketAddr {
-        SocketAddr::new(self.devserver_ip, self.devserver_port)
+        SocketAddr::new(self.devserver_exposed_ip, self.devserver_port)
     }
 
     // Get the address the server should run on if we're serving the user's server
     pub fn proxied_server_address(&self) -> Option<SocketAddr> {
         self.proxied_port
-            .map(|port| SocketAddr::new(self.devserver_ip, port))
+            .map(|port| SocketAddr::new(self.devserver_exposed_ip, port))
     }
 
     pub fn server_address(&self) -> Option<SocketAddr> {
@@ -330,6 +348,19 @@ impl WebServer {
             _ => self.proxied_server_address(),
         }
     }
+
+    /// Get the address the server is running - showing 127.0.0.1 if the devserver is bound to 0.0.0.0
+    /// This is designed this way to not confuse users who expect the devserver to be bound to localhost
+    /// ... which it is, but they don't know that 0.0.0.0 also serves localhost.
+    pub fn displayed_address(&self) -> Option<SocketAddr> {
+        let mut address = self.server_address()?;
+
+        if self.devserver_bind_ip == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) {
+            address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), address.port());
+        }
+
+        Some(address)
+    }
 }
 
 async fn devserver_mainloop(
@@ -382,6 +413,7 @@ fn build_devserver_router(
     if args.should_proxy_build() {
         // For fullstack, liveview, and server, forward all requests to the inner server
         let address = fullstack_address.unwrap();
+        tracing::debug!("Proxying requests to fullstack server at {address}");
         router = router.nest_service("/",super::proxy::proxy_to(
             format!("http://{address}").parse().unwrap(),
             true,
@@ -616,7 +648,15 @@ async fn get_rustls(web_config: &WebHttpsConfig) -> Result<(String, String)> {
 ///
 /// Todo: we might want to do this on every new build in case the OS tries to bind things to this port
 /// and we don't already have something bound to it. There's no great way of "reserving" a port.
-fn get_available_port(address: IpAddr) -> Option<u16> {
+fn get_available_port(address: IpAddr, prefer: Option<u16>) -> Option<u16> {
+    // First, try to bind to the preferred port
+    if let Some(port) = prefer {
+        if let Ok(_listener) = TcpListener::bind((address, port)) {
+            return Some(port);
+        }
+    }
+
+    // Otherwise, try to bind to any port and return the first one we can
     TcpListener::bind((address, 0))
         .map(|listener| listener.local_addr().unwrap().port())
         .ok()

+ 2 - 2
packages/cli/src/wasm_bindgen.rs

@@ -327,7 +327,7 @@ impl WasmBindgen {
     }
 
     async fn verify_local_install(&self) -> anyhow::Result<()> {
-        tracing::info!(
+        tracing::trace!(
             "Verifying wasm-bindgen-cli@{} is installed in the path",
             self.version
         );
@@ -355,7 +355,7 @@ impl WasmBindgen {
     }
 
     async fn verify_managed_install(&self) -> anyhow::Result<()> {
-        tracing::info!(
+        tracing::trace!(
             "Verifying wasm-bindgen-cli@{} is installed in the tool directory",
             self.version
         );

+ 4 - 0
packages/desktop/src/app.rs

@@ -485,6 +485,10 @@ impl App {
     // Write this to the target dir so we can pick back up
     #[cfg(debug_assertions)]
     fn resume_from_state(&mut self, webview: &WebviewInstance) {
+        if cfg!(target_os = "android") || cfg!(target_os = "ios") {
+            return;
+        }
+
         if let Ok(state) = std::fs::read_to_string(restore_file()) {
             if let Ok(state) = serde_json::from_str::<PreservedWindowState>(&state) {
                 let window = &webview.desktop_context.window;

+ 1 - 1
packages/desktop/src/protocol.rs

@@ -288,7 +288,7 @@ pub(crate) fn to_java_load_asset(filepath: &str) -> Option<Vec<u8>> {
         }
     }
 
-    use std::{io::Read, ptr::NonNull};
+    use std::ptr::NonNull;
 
     let ctx = ndk_context::android_context();
     let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();

+ 1 - 1
packages/dioxus/src/launch.rs

@@ -87,7 +87,7 @@ impl LaunchBuilder {
     #[cfg_attr(docsrs, doc(cfg(feature = "mobile")))]
     pub fn mobile() -> LaunchBuilder {
         LaunchBuilder {
-            launch_fn: |root, contexts, cfg| dioxus_mobile::launch::launch(root, contexts, cfg),
+            launch_fn: |root, contexts, cfg| dioxus_mobile::launch_cfg(root, contexts, cfg),
             contexts: Vec::new(),
             configs: Vec::new(),
         }

+ 54 - 10
packages/mobile/src/lib.rs

@@ -4,18 +4,18 @@
 
 pub use dioxus_desktop::*;
 use dioxus_lib::prelude::*;
+use std::any::Any;
 use std::sync::Mutex;
 
 pub mod launch_bindings {
-    use std::any::Any;
 
     use super::*;
     pub fn launch(
         root: fn() -> Element,
-        _contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
-        _platform_config: Vec<Box<dyn Any>>,
+        contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
+        platform_config: Vec<Box<dyn Any>>,
     ) {
-        super::launch(root);
+        super::launch_cfg(root, contexts, platform_config);
     }
 
     pub fn launch_virtual_dom(_virtual_dom: VirtualDom, _desktop_config: Config) -> ! {
@@ -24,27 +24,71 @@ pub mod launch_bindings {
 }
 
 /// Launch via the binding API
-pub fn launch(incoming: fn() -> Element) {
+pub fn launch(root: fn() -> Element) {
+    launch_cfg(root, vec![], vec![]);
+}
+
+pub fn launch_cfg(
+    root: fn() -> Element,
+    contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
+    platform_config: Vec<Box<dyn Any>>,
+) {
     #[cfg(target_os = "android")]
     {
-        *APP_FN_PTR.lock().unwrap() = Some(incoming);
+        *APP_OBJECTS.lock().unwrap() = Some(BoundLaunchObjects {
+            root,
+            contexts,
+            platform_config,
+        });
     }
 
     #[cfg(not(target_os = "android"))]
     {
-        dioxus_desktop::launch::launch(incoming, vec![], Default::default());
+        dioxus_desktop::launch::launch(root, contexts, platform_config);
     }
 }
 
-static APP_FN_PTR: Mutex<Option<fn() -> Element>> = Mutex::new(None);
+/// We need to store the root function and contexts in a static so that when the tao bindings call
+/// "start_app", that the original function arguments are still around.
+///
+/// If you look closely, you'll notice that we impl Send for this struct. This would normally be
+/// unsound. However, we know that the thread that created these objects ("main()" - see JNI_OnLoad)
+/// is finished once `start_app` is called. This is similar to how an Rc<T> is technically safe
+/// to move between threads if you can prove that no other thread is using the Rc<T> at the same time.
+/// Crates like https://crates.io/crates/sendable exist that build on this idea but with runtimk,
+/// validation that the current thread is the one that created the object.
+///
+/// Since `main()` completes, the only reader of this data will be `start_app`, so it's okay to
+/// impl this as Send/Sync.
+///
+/// Todo(jon): the visibility of functions in this module is too public. Make sure to hide them before
+/// releasing 0.7.
+struct BoundLaunchObjects {
+    root: fn() -> Element,
+    contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
+    platform_config: Vec<Box<dyn Any>>,
+}
+
+unsafe impl Send for BoundLaunchObjects {}
+unsafe impl Sync for BoundLaunchObjects {}
 
+static APP_OBJECTS: Mutex<Option<BoundLaunchObjects>> = Mutex::new(None);
+
+#[doc(hidden)]
 pub fn root() {
-    let app = APP_FN_PTR
+    let app = APP_OBJECTS
         .lock()
         .expect("APP_FN_PTR lock failed")
+        .take()
         .expect("Android to have set the app trampoline");
 
-    dioxus_desktop::launch::launch(app, vec![], Default::default());
+    let BoundLaunchObjects {
+        root,
+        contexts,
+        platform_config,
+    } = app;
+
+    dioxus_desktop::launch::launch(root, contexts, platform_config);
 }
 
 /// Expose the `Java_dev_dioxus_main_WryActivity_create` function to the JNI layer.