Quellcode durchsuchen

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
Jonathan Kelley vor 1 Monat
Ursprung
Commit
4d3d55a7a3
56 geänderte Dateien mit 1559 neuen und 547 gelöschten Zeilen
  1. 56 0
      .github/install.ps1
  2. 104 0
      .github/install.sh
  3. 1 1
      .github/workflows/publish.yml
  4. 109 58
      Cargo.lock
  5. 56 56
      Cargo.toml
  6. 3 0
      _typos.toml
  7. 1 1
      example-projects/ecommerce-site/README.md
  8. 0 0
      example-projects/ecommerce-site/tailwind.css
  9. 0 0
      examples/tailwind/tailwind.css
  10. 2 1
      lychee.toml
  11. 5 2
      packages/cli-config/src/lib.rs
  12. 3 4
      packages/cli/Cargo.toml
  13. 0 2
      packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs
  14. 57 30
      packages/cli/src/build/builder.rs
  15. 189 45
      packages/cli/src/build/request.rs
  16. 19 3
      packages/cli/src/build/tools.rs
  17. 3 0
      packages/cli/src/cli/build.rs
  18. 1 1
      packages/cli/src/cli/create.rs
  19. 59 22
      packages/cli/src/cli/mod.rs
  20. 1 1
      packages/cli/src/cli/run.rs
  21. 247 0
      packages/cli/src/cli/update.rs
  22. 23 5
      packages/cli/src/config/app.rs
  23. 0 5
      packages/cli/src/config/desktop.rs
  24. 5 6
      packages/cli/src/config/dioxus_config.rs
  25. 0 2
      packages/cli/src/config/mod.rs
  26. 1 1
      packages/cli/src/main.rs
  27. 17 17
      packages/cli/src/serve/mod.rs
  28. 16 11
      packages/cli/src/serve/output.rs
  29. 19 6
      packages/cli/src/serve/runner.rs
  30. 4 0
      packages/cli/src/serve/server.rs
  31. 2 0
      packages/cli/src/settings.rs
  32. 11 6
      packages/cli/src/tailwind.rs
  33. 1 1
      packages/cli/src/wasm_bindgen.rs
  34. 69 5
      packages/cli/src/workspace.rs
  35. 0 3
      packages/core-macro/src/component.rs
  36. 0 2
      packages/core/src/lib.rs
  37. 0 29
      packages/core/src/properties.rs
  38. 1 8
      packages/core/src/virtual_dom.rs
  39. 1 1
      packages/desktop/Cargo.toml
  40. 91 13
      packages/desktop/src/app.rs
  41. 184 0
      packages/desktop/src/assets/dev.index.html
  42. 0 0
      packages/desktop/src/assets/prod.index.html
  43. 104 33
      packages/desktop/src/launch.rs
  44. 5 1
      packages/desktop/src/protocol.rs
  45. 26 1
      packages/desktop/src/webview.rs
  46. 27 1
      packages/devtools/src/lib.rs
  47. 1 1
      packages/dioxus/Cargo.toml
  48. 2 2
      packages/dioxus/src/launch.rs
  49. 1 3
      packages/liveview/src/pool.rs
  50. 1 139
      packages/mobile/src/lib.rs
  51. 5 7
      packages/native/src/lib.rs
  52. 1 5
      packages/server/src/launch.rs
  53. 1 3
      packages/subsecond/subsecond/src/lib.rs
  54. 8 1
      packages/wasm-split/wasm-split-cli/Cargo.toml
  55. 8 1
      packages/wasm-split/wasm-split-macro/Cargo.toml
  56. 8 1
      packages/wasm-split/wasm-split/Cargo.toml

+ 56 - 0
.github/install.ps1

@@ -0,0 +1,56 @@
+#!/usr/bin/env pwsh
+
+$ErrorActionPreference = 'Stop'
+
+if ($v) {
+  $Version = "v${v}"
+}
+if ($Args.Length -eq 1) {
+  $Version = $Args.Get(0)
+}
+
+$DxInstall = $env:DX_INSTALL
+$BinDir = if ($DxInstall) {
+  "${DxInstall}\bin"
+} else {
+  "${Home}\.dx\bin"
+}
+
+$DxZip = "$BinDir\dx.zip"
+$DxExe = "$BinDir\dx.exe"
+$Target = 'x86_64-pc-windows-msvc'
+
+$DownloadUrl = if (!$Version) {
+    "https://github.com/dioxuslabs/dioxus/releases/latest/download/dx-${target}.zip"
+} else {
+    "https://github.com/dioxuslabs/dioxus/releases/download/${Version}/dx-${target}.zip"
+}
+
+
+if (!(Test-Path $BinDir)) {
+  New-Item $BinDir -ItemType Directory | Out-Null
+}
+
+curl.exe --ssl-revoke-best-effort -Lo $DxZip $DownloadUrl
+
+tar.exe xf $DxZip -C $BinDir
+
+Remove-Item $DxZip
+
+$CargoBin = "${Home}\.cargo\bin"
+
+if (!(Test-Path $CargoBin)) {
+    New-Item $CargoBin -ItemType Directory | Out-Null
+}
+
+Copy-Item $DxExe "$CargoBin\dx.exe" -Force
+
+# $User = [System.EnvironmentVariableTarget]::User
+# $Path = [System.Environment]::GetEnvironmentVariable('Path', $User)
+# if (!(";${Path};".ToLower() -like "*;${BinDir};*".ToLower())) {
+#   [System.Environment]::SetEnvironmentVariable('Path', "${Path};${BinDir}", $User)
+#   $Env:Path += ";${BinDir}"
+# }
+
+Write-Output "dx was installed successfully! 💫"
+Write-Output "Run 'dx --help' to get started"

+ 104 - 0
.github/install.sh

@@ -0,0 +1,104 @@
+#!/bin/sh
+set -eo pipefail
+
+# Reset
+Color_Off=''
+
+# Regular Colors
+Red=''
+Green=''
+Dim='' # White
+
+# Bold
+Bold_White=''
+Bold_Green=''
+
+if [[ -t 1 ]]; then
+    # Reset
+    Color_Off='\033[0m' # Text Reset
+
+    # Regular Colors
+    Red='\033[0;31m'   # Red
+    Green='\033[0;32m' # Green
+    Dim='\033[0;2m'    # White
+
+    # Bold
+    Bold_Green='\033[1;32m' # Bold Green
+    Bold_White='\033[1m'    # Bold White
+fi
+
+error() {
+    echo -e "${Red}error${Color_Off}:" "$@" >&2
+    exit 1
+}
+
+info() {
+    echo -e "${Dim}$@ ${Color_Off}"
+}
+
+info_bold() {
+    echo -e "${Bold_White}$@ ${Color_Off}"
+}
+
+success() {
+    echo -e "${Green}$@ ${Color_Off}"
+}
+
+command -v unzip >/dev/null ||
+    error 'unzip is required to install dx'
+
+if [[ $# -gt 1 ]]; then
+    error 'Too many arguments, only 1 are allowed. The first can be a specific tag of dx to install. (e.g. "dx-v0.7.1")'
+fi
+
+if [ "$OS" = "Windows_NT" ]; then
+	target="x86_64-pc-windows-msvc"
+else
+	case $(uname -sm) in
+	"Darwin x86_64") target="x86_64-apple-darwin" ;;
+	"Darwin arm64") target="aarch64-apple-darwin" ;;
+	"Linux aarch64") target="aarch64-unknown-linux-gnu" ;;
+	*) target="x86_64-unknown-linux-gnu" ;;
+	esac
+fi
+
+GITHUB=${GITHUB-"https://github.com"}
+github_repo="$GITHUB/dioxuslabs/dioxus"
+exe_name=dx
+
+if [[ $# = 0 ]]; then
+    dx_uri=$github_repo/releases/latest/download/dx-$target.zip
+else
+    dx_uri=$github_repo/releases/download/$1/dx-$target.zip
+fi
+
+dx_install="${DX_INSTALL:-$HOME/.dx}"
+bin_dir="$dx_install/bin"
+exe="$bin_dir/dx"
+cargo_bin_dir="$HOME/.cargo/bin"
+cargo_bin_exe="$cargo_bin_dir/dx"
+
+if [ ! -d "$bin_dir" ]; then
+	mkdir -p "$bin_dir"
+fi
+
+curl --fail --location --progress-bar --output "$exe.zip" "$dx_uri"
+if command -v unzip >/dev/null; then
+	unzip -d "$bin_dir" -o "$exe.zip"
+else
+	7z x -o"$bin_dir" -y "$exe.zip"
+fi
+chmod +x "$exe"
+cp "$exe" "$cargo_bin_exe" || error "Failed to copy dx to $cargo_bin_dir"
+rm "$exe.zip"
+echo "  installed: $cargo_bin_exe"
+
+echo
+echo "dx was installed successfully! 💫"
+echo
+
+if command -v dx >/dev/null; then
+	echo "Run 'dx --help' to get started"
+else
+	echo "Run '$exe --help' to get started"
+fi

+ 1 - 1
.github/workflows/publish.yml

@@ -98,7 +98,7 @@ jobs:
           bin: dx
           token: ${{ secrets.GITHUB_TOKEN }}
           target: ${{ matrix.platform.target }}
-          archive: $bin-$target-${{ env.RELEASE_TAG }}
+          archive: $bin-$target
           checksum: sha256
           manifest_path: packages/cli/Cargo.toml
           ref: refs/tags/${{ env.RELEASE_POST }}

+ 109 - 58
Cargo.lock

@@ -2624,7 +2624,7 @@ dependencies = [
 
 [[package]]
 name = "const-serialize"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "const-serialize",
  "const-serialize-macro",
@@ -2634,7 +2634,7 @@ dependencies = [
 
 [[package]]
 name = "const-serialize-macro"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3398,7 +3398,7 @@ dependencies = [
 
 [[package]]
 name = "depinfo"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "thiserror 2.0.12",
 ]
@@ -3569,7 +3569,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "criterion",
  "dioxus",
@@ -3610,7 +3610,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-asset-resolver"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus-cli-config",
  "http 1.3.1",
@@ -3625,7 +3625,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-autofmt"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus-rsx",
  "pretty_assertions",
@@ -3638,7 +3638,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-check"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "indoc",
  "owo-colors",
@@ -3650,7 +3650,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-cli"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "ansi-to-html",
  "ansi-to-tui",
@@ -3725,6 +3725,8 @@ dependencies = [
  "regex",
  "reqwest 0.12.15",
  "rustls 0.23.27",
+ "self-replace",
+ "self_update",
  "serde",
  "serde_json",
  "shell-words",
@@ -3762,14 +3764,14 @@ dependencies = [
 
 [[package]]
 name = "dioxus-cli-config"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "wasm-bindgen",
 ]
 
 [[package]]
 name = "dioxus-cli-opt"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "anyhow",
  "browserslist-rs 0.16.0",
@@ -3828,7 +3830,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-config-macro"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3836,11 +3838,11 @@ dependencies = [
 
 [[package]]
 name = "dioxus-config-macros"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 
 [[package]]
 name = "dioxus-core"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "const_format",
  "dioxus",
@@ -3871,7 +3873,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-core-macro"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "convert_case 0.8.0",
  "dioxus",
@@ -3887,14 +3889,14 @@ dependencies = [
 
 [[package]]
 name = "dioxus-core-types"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "once_cell",
 ]
 
 [[package]]
 name = "dioxus-desktop"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "async-trait",
  "base64 0.22.1",
@@ -3922,6 +3924,7 @@ dependencies = [
  "infer",
  "jni",
  "lazy-js-bundle",
+ "libc",
  "muda 0.16.1",
  "ndk",
  "ndk-context",
@@ -3951,7 +3954,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-devtools"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus-cli-config",
  "dioxus-core",
@@ -3969,7 +3972,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-devtools-types"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus-core",
  "serde",
@@ -3978,7 +3981,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-document"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus",
  "dioxus-core",
@@ -3996,7 +3999,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-dx-wire-format"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "cargo_metadata",
  "serde",
@@ -4005,7 +4008,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-examples"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "async-std",
  "base64 0.22.1",
@@ -4031,7 +4034,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-ext"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus-autofmt",
  "dioxus-rsx-rosetta",
@@ -4042,7 +4045,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-fullstack"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "async-trait",
  "aws-lc-rs",
@@ -4088,7 +4091,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-fullstack-hooks"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus",
  "dioxus-core",
@@ -4103,7 +4106,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-fullstack-protocol"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "base64 0.22.1",
  "ciborium",
@@ -4114,7 +4117,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-history"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus",
  "dioxus-core",
@@ -4123,7 +4126,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-hooks"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus",
  "dioxus-core",
@@ -4142,7 +4145,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-html"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "async-trait",
  "dioxus",
@@ -4171,7 +4174,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-html-internal-macro"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "convert_case 0.8.0",
  "proc-macro2",
@@ -4182,7 +4185,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-interpreter-js"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus-core",
  "dioxus-core-types",
@@ -4200,7 +4203,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-isrg"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "chrono",
  "http 1.3.1",
@@ -4213,7 +4216,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-lib"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus",
  "dioxus-config-macro",
@@ -4230,7 +4233,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-liveview"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "axum 0.8.4",
  "dioxus",
@@ -4258,7 +4261,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-logger"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "console_error_panic_hook",
  "dioxus",
@@ -4270,7 +4273,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-mobile"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus-cli-config",
  "dioxus-desktop",
@@ -4282,7 +4285,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-native"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "blitz-dom",
  "blitz-net",
@@ -4377,7 +4380,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-router"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "axum 0.8.4",
  "base64 0.22.1",
@@ -4400,7 +4403,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-router-macro"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "base16",
  "digest",
@@ -4414,7 +4417,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-rsx"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "prettier-please",
  "prettyplease",
@@ -4426,7 +4429,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-rsx-hotreload"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus-core",
  "dioxus-core-types",
@@ -4441,7 +4444,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-rsx-rosetta"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "convert_case 0.8.0",
  "dioxus-autofmt",
@@ -4457,7 +4460,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-server"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "async-trait",
  "aws-lc-rs",
@@ -4507,7 +4510,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-signals"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dioxus",
  "dioxus-core",
@@ -4528,7 +4531,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-ssr"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "askama_escape",
  "dioxus",
@@ -4547,7 +4550,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus-web"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "async-trait",
  "ciborium",
@@ -4585,7 +4588,7 @@ dependencies = [
 
 [[package]]
 name = "dioxus_server_macro"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "axum 0.8.4",
  "dioxus",
@@ -4854,6 +4857,7 @@ dependencies = [
  "ed25519",
  "serde",
  "sha2",
+ "signature",
  "subtle",
  "zeroize",
 ]
@@ -5822,7 +5826,7 @@ dependencies = [
 
 [[package]]
 name = "generational-box"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "criterion",
  "parking_lot",
@@ -8126,7 +8130,7 @@ dependencies = [
 
 [[package]]
 name = "lazy-js-bundle"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 
 [[package]]
 name = "lazy_static"
@@ -8578,7 +8582,7 @@ dependencies = [
 
 [[package]]
 name = "manganis"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "const-serialize",
  "manganis-core",
@@ -8587,7 +8591,7 @@ dependencies = [
 
 [[package]]
 name = "manganis-core"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "const-serialize",
  "dioxus",
@@ -8599,7 +8603,7 @@ dependencies = [
 
 [[package]]
 name = "manganis-macro"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "dunce",
  "macro-string",
@@ -12168,6 +12172,41 @@ dependencies = [
  "to_shmem_derive",
 ]
 
+[[package]]
+name = "self-replace"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7"
+dependencies = [
+ "fastrand",
+ "tempfile",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "self_update"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d832c086ece0dacc29fb2947bb4219b8f6e12fe9e40b7108f9e57c4224e47b5c"
+dependencies = [
+ "either",
+ "flate2",
+ "hyper 1.6.0",
+ "indicatif",
+ "log",
+ "quick-xml 0.37.5",
+ "regex",
+ "reqwest 0.12.15",
+ "self-replace",
+ "semver",
+ "serde_json",
+ "tar",
+ "tempfile",
+ "urlencoding",
+ "zip 2.6.1",
+ "zipsign-api",
+]
+
 [[package]]
 name = "semver"
 version = "1.0.26"
@@ -13414,7 +13453,7 @@ dependencies = [
 
 [[package]]
 name = "subsecond"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "js-sys",
  "libc",
@@ -13431,7 +13470,7 @@ dependencies = [
 
 [[package]]
 name = "subsecond-types"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "serde",
 ]
@@ -13463,7 +13502,7 @@ dependencies = [
 
 [[package]]
 name = "suspense-carousel"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "async-std",
  "dioxus",
@@ -15849,7 +15888,7 @@ dependencies = [
 
 [[package]]
 name = "wasm-split"
-version = "0.1.0"
+version = "0.7.0-alpha.0"
 dependencies = [
  "async-once-cell",
  "wasm-split-macro",
@@ -15857,7 +15896,7 @@ dependencies = [
 
 [[package]]
 name = "wasm-split-cli"
-version = "0.1.0"
+version = "0.7.0-alpha.0"
 dependencies = [
  "anyhow",
  "clap",
@@ -15891,7 +15930,7 @@ dependencies = [
 
 [[package]]
 name = "wasm-split-macro"
-version = "0.1.0"
+version = "0.7.0-alpha.0"
 dependencies = [
  "base16",
  "digest",
@@ -15916,7 +15955,7 @@ dependencies = [
 
 [[package]]
 name = "wasm-used"
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 dependencies = [
  "id-arena",
  "tracing",
@@ -17737,6 +17776,7 @@ dependencies = [
  "flate2",
  "indexmap 2.9.0",
  "memchr",
+ "time",
  "zopfli",
 ]
 
@@ -17751,6 +17791,17 @@ dependencies = [
  "thiserror 1.0.69",
 ]
 
+[[package]]
+name = "zipsign-api"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e7c724c3a8e5833aad6b7028f4f0989fa3a640ce799bf8c352f417b8ef9db3e"
+dependencies = [
+ "base64 0.22.1",
+ "ed25519-dalek",
+ "thiserror 2.0.12",
+]
+
 [[package]]
 name = "zopfli"
 version = "0.8.2"

+ 56 - 56
Cargo.toml

@@ -127,71 +127,71 @@ members = [
 ]
 
 [workspace.package]
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 
 # dependencies that are shared across packages
 [workspace.dependencies]
-dioxus = { path = "packages/dioxus", version = "0.6.2" }
-dioxus-lib = { path = "packages/dioxus-lib", version = "0.6.2" }
-dioxus-core = { path = "packages/core", version = "0.6.2" }
-dioxus-core-types = { path = "packages/core-types", version = "0.6.2" }
-dioxus-core-macro = { path = "packages/core-macro", version = "0.6.2" }
-dioxus-config-macro = { path = "packages/config-macro", version = "0.6.2" }
-dioxus-router = { path = "packages/router", version = "0.6.2" }
-dioxus-router-macro = { path = "packages/router-macro", version = "0.6.2" }
-dioxus-document = { path = "packages/document", version = "0.6.2", default-features = false }
-dioxus-history = { path = "packages/history", version = "0.6.2", default-features = false }
-dioxus-html = { path = "packages/html", version = "0.6.2", default-features = false }
-dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.6.2" }
-dioxus-hooks = { path = "packages/hooks", version = "0.6.2" }
-dioxus-web = { path = "packages/web", version = "0.6.2", default-features = false }
-dioxus-isrg = { path = "packages/isrg", version = "0.6.2" }
-dioxus-ssr = { path = "packages/ssr", version = "0.6.2", default-features = false }
-dioxus-desktop = { path = "packages/desktop", version = "0.6.2", default-features = false }
-dioxus-mobile = { path = "packages/mobile", version = "0.6.2" }
-dioxus-interpreter-js = { path = "packages/interpreter", version = "0.6.2" }
-dioxus-liveview = { path = "packages/liveview", version = "0.6.2" }
-dioxus-autofmt = { path = "packages/autofmt", version = "0.6.2" }
-dioxus-check = { path = "packages/check", version = "0.6.2" }
-dioxus-rsx = { path = "packages/rsx", version = "0.6.2" }
-dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.6.2" }
-dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.6.2" }
-dioxus-signals = { path = "packages/signals", version = "0.6.2" }
-dioxus-cli-config = { path = "packages/cli-config", version = "0.6.2" }
-dioxus-cli-opt = { path = "packages/cli-opt", version = "0.6.2" }
-dioxus-devtools = { path = "packages/devtools", version = "0.6.2" }
-dioxus-devtools-types = { path = "packages/devtools-types", version = "0.6.2" }
-dioxus-server = { path = "packages/server", version = "0.6.2" }
-dioxus-fullstack = { path = "packages/fullstack", version = "0.6.2" }
-dioxus-fullstack-hooks = { path = "packages/fullstack-hooks", version = "0.6.3" }
-dioxus-fullstack-protocol = { path = "packages/fullstack-protocol", version = "0.6.3" }
-dioxus_server_macro = { path = "packages/server-macro", version = "0.6.2", default-features = false }
-dioxus-dx-wire-format = { path = "packages/dx-wire-format", version = "0.6.2" }
-dioxus-logger = { path = "packages/logger", version = "0.6.2" }
-dioxus-native = { path = "packages/native", version = "0.6.2" }
-dioxus-asset-resolver = { path = "packages/asset-resolver", version = "0.6.2" }
-dioxus-config-macros = { path = "packages/config-macros", version = "0.6.3" }
-const-serialize = { path = "packages/const-serialize", version = "0.6.2" }
-const-serialize-macro = { path = "packages/const-serialize-macro", version = "0.6.2" }
-generational-box = { path = "packages/generational-box", version = "0.6.2" }
-lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.6.2" }
+dioxus = { path = "packages/dioxus", version = "0.7.0-alpha.0" }
+dioxus-lib = { path = "packages/dioxus-lib", version = "0.7.0-alpha.0" }
+dioxus-core = { path = "packages/core", version = "0.7.0-alpha.0" }
+dioxus-core-types = { path = "packages/core-types", version = "0.7.0-alpha.0" }
+dioxus-core-macro = { path = "packages/core-macro", version = "0.7.0-alpha.0" }
+dioxus-config-macro = { path = "packages/config-macro", version = "0.7.0-alpha.0" }
+dioxus-router = { path = "packages/router", version = "0.7.0-alpha.0" }
+dioxus-router-macro = { path = "packages/router-macro", version = "0.7.0-alpha.0" }
+dioxus-document = { path = "packages/document", version = "0.7.0-alpha.0", default-features = false }
+dioxus-history = { path = "packages/history", version = "0.7.0-alpha.0", default-features = false }
+dioxus-html = { path = "packages/html", version = "0.7.0-alpha.0", default-features = false }
+dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.7.0-alpha.0" }
+dioxus-hooks = { path = "packages/hooks", version = "0.7.0-alpha.0" }
+dioxus-web = { path = "packages/web", version = "0.7.0-alpha.0", default-features = false }
+dioxus-isrg = { path = "packages/isrg", version = "0.7.0-alpha.0" }
+dioxus-ssr = { path = "packages/ssr", version = "0.7.0-alpha.0", default-features = false }
+dioxus-desktop = { path = "packages/desktop", version = "0.7.0-alpha.0", default-features = false }
+dioxus-mobile = { path = "packages/mobile", version = "0.7.0-alpha.0" }
+dioxus-interpreter-js = { path = "packages/interpreter", version = "0.7.0-alpha.0" }
+dioxus-liveview = { path = "packages/liveview", version = "0.7.0-alpha.0" }
+dioxus-autofmt = { path = "packages/autofmt", version = "0.7.0-alpha.0" }
+dioxus-check = { path = "packages/check", version = "0.7.0-alpha.0" }
+dioxus-rsx = { path = "packages/rsx", version = "0.7.0-alpha.0" }
+dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.7.0-alpha.0" }
+dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.7.0-alpha.0" }
+dioxus-signals = { path = "packages/signals", version = "0.7.0-alpha.0" }
+dioxus-cli-config = { path = "packages/cli-config", version = "0.7.0-alpha.0" }
+dioxus-cli-opt = { path = "packages/cli-opt", version = "0.7.0-alpha.0" }
+dioxus-devtools = { path = "packages/devtools", version = "0.7.0-alpha.0" }
+dioxus-devtools-types = { path = "packages/devtools-types", version = "0.7.0-alpha.0" }
+dioxus-server = { path = "packages/server", version = "0.7.0-alpha.0" }
+dioxus-fullstack = { path = "packages/fullstack", version = "0.7.0-alpha.0" }
+dioxus-fullstack-hooks = { path = "packages/fullstack-hooks", version = "0.7.0-alpha.0" }
+dioxus-fullstack-protocol = { path = "packages/fullstack-protocol", version = "0.7.0-alpha.0" }
+dioxus_server_macro = { path = "packages/server-macro", version = "0.7.0-alpha.0", default-features = false }
+dioxus-dx-wire-format = { path = "packages/dx-wire-format", version = "0.7.0-alpha.0" }
+dioxus-logger = { path = "packages/logger", version = "0.7.0-alpha.0" }
+dioxus-native = { path = "packages/native", version = "0.7.0-alpha.0" }
+dioxus-asset-resolver = { path = "packages/asset-resolver", version = "0.7.0-alpha.0" }
+dioxus-config-macros = { path = "packages/config-macros", version = "0.7.0-alpha.0" }
+const-serialize = { path = "packages/const-serialize", version = "0.7.0-alpha.0" }
+const-serialize-macro = { path = "packages/const-serialize-macro", version = "0.7.0-alpha.0" }
+generational-box = { path = "packages/generational-box", version = "0.7.0-alpha.0" }
+lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.7.0-alpha.0" }
 
 # subsecond
-subsecond-types = { path = "packages/subsecond/subsecond-types", version = "0.6.3" }
-subsecond = { path = "packages/subsecond/subsecond", version = "0.6.3" }
+subsecond-types = { path = "packages/subsecond/subsecond-types", version = "0.7.0-alpha.0" }
+subsecond = { path = "packages/subsecond/subsecond", version = "0.7.0-alpha.0" }
 
 # manganis
-manganis = { path = "packages/manganis/manganis", version = "0.6.2" }
-manganis-core = { path = "packages/manganis/manganis-core", version = "0.6.2" }
-manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.6.2" }
+manganis = { path = "packages/manganis/manganis", version = "0.7.0-alpha.0" }
+manganis-core = { path = "packages/manganis/manganis-core", version = "0.7.0-alpha.0" }
+manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.7.0-alpha.0" }
 
 # wasm-split
-wasm-split = { path = "packages/wasm-split/wasm-split", version = "0.1.0" }
-wasm-split-macro = { path = "packages/wasm-split/wasm-split-macro", version = "0.1.0" }
-wasm-split-cli = { path = "packages/wasm-split/wasm-split-cli", version = "0.1.0" }
-wasm-split-harness = { path = "packages/playwright-tests/wasm-split-harness", version = "0.1.0" }
+wasm-split = { path = "packages/wasm-split/wasm-split", version = "0.7.0-alpha.0" }
+wasm-split-macro = { path = "packages/wasm-split/wasm-split-macro", version = "0.7.0-alpha.0" }
+wasm-split-cli = { path = "packages/wasm-split/wasm-split-cli", version = "0.7.0-alpha.0" }
+wasm-split-harness = { path = "packages/playwright-tests/wasm-split-harness", version = "0.7.0-alpha.0" }
 
-depinfo = { path = "packages/depinfo", version = "0.6.3" }
+depinfo = { path = "packages/depinfo", version = "0.7.0-alpha.0" }
 warnings = { version = "0.2.1" }
 
 # a fork of pretty please for tests - let's get off of this if we can!
@@ -400,7 +400,7 @@ documentation = "https://dioxuslabs.com"
 keywords = ["dom", "ui", "gui", "react", "wasm"]
 rust-version = "1.80.0"
 publish = false
-version = "0.6.3"
+version = "0.7.0-alpha.0"
 
 [dependencies]
 reqwest = { workspace = true, features = ["json"], optional = true }

+ 3 - 0
_typos.toml

@@ -5,6 +5,9 @@ ratatui = "ratatui"
 lits = "lits"
 # https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeked_event
 seeked = "seeked"
+# https://developer.apple.com/forums/thread/108953
+# udid = unique device identifier
+udid = "udid"
 
 [files]
 extend-exclude = ["translations/*", "CHANGELOG.md", "*.js"]

+ 1 - 1
example-projects/ecommerce-site/README.md

@@ -9,7 +9,7 @@ This example app is a fullstack web application leveraging the [FakeStoreAPI](ht
 1. Run the following commands to serve the application (see the tailwind example in the main Dioxus repo for more detailed information about setting up tailwind):
 
 ```bash
-npx tailwindcss -i ./input.css -o ./public/tailwind.css --watch
+npx tailwindcss -i ./tailwind.css -o ./public/tailwind.css --watch
 dx serve
 ```
 

+ 0 - 0
example-projects/ecommerce-site/input.css → example-projects/ecommerce-site/tailwind.css


+ 0 - 0
examples/tailwind/input.css → examples/tailwind/tailwind.css


+ 2 - 1
lychee.toml

@@ -3,4 +3,5 @@ exclude = ['file:///', 'https://github.com/DioxusLabs/dioxus/commit']
 exclude_path = ['target']
 no_progress = false
 cache = true
-max_cache_age = "10d"
+max_cache_age = "10d"
+accept = ["200..=204", "429"]

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

@@ -102,15 +102,18 @@ 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> {
-    let ip = std::env::var(DEVSERVER_IP_ENV).ok()?;
-    let port = std::env::var(DEVSERVER_PORT_ENV).ok()?;
+    let port = std::env::var(DEVSERVER_PORT_ENV).ok();
 
     if cfg!(target_os = "android") {
         // Since `adb reverse` is used for Android, the 127.0.0.1 will always be
         // the correct IP address.
+        let port = port.unwrap_or("8080".to_string());
         return Some(format!("127.0.0.1:{}", port).parse().unwrap());
     }
 
+    let port = port?;
+    let ip = std::env::var(DEVSERVER_IP_ENV).ok()?;
+
     format!("{}:{}", ip, port).parse().ok()
 }
 

+ 3 - 4
packages/cli/Cargo.toml

@@ -136,6 +136,8 @@ backtrace = "0.3.74"
 ar = "0.9.0"
 wasm-bindgen-externref-xform = "0.2.100"
 pdb = "0.8.0"
+self_update = { version = "0.42.0", features = ["archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate"] }
+self-replace = "1.5.0"
 
 [build-dependencies]
 built = { version = "0.7.5", features = ["git2"] }
@@ -162,10 +164,7 @@ name = "dx"
 escargot = "0.5"
 
 [package.metadata.binstall]
-pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }-v{ version }{ archive-suffix }"
-pkg-fmt = "tgz"
-
-[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
+pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }{ archive-suffix }"
 pkg-fmt = "zip"
 
 [package.metadata.docs.rs]

+ 0 - 2
packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs

@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
-
     <uses-permission android:name="android.permission.INTERNET" />
     <application android:hasCode="true" android:supportsRtl="true" android:icon="@mipmap/ic_launcher"
         android:extractNativeLibs="true"
@@ -16,5 +15,4 @@
             </intent-filter>
         </activity>
     </application>
-
 </manifest>

+ 57 - 30
packages/cli/src/build/builder.rs

@@ -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
packages/cli/src/build/request.rs

@@ -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
packages/cli/src/build/tools.rs

@@ -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)
 }

+ 3 - 0
packages/cli/src/cli/build.rs

@@ -89,6 +89,9 @@ impl BuildArgs {
     pub async fn into_targets(self) -> Result<BuildTargets> {
         let workspace = Workspace::current().await?;
 
+        // do some logging to ensure dx matches the dioxus version since we're not always API compatible
+        workspace.check_dioxus_version_against_cli();
+
         let mut server = None;
 
         let client = match self.targets {

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

@@ -221,7 +221,7 @@ pub(crate) fn post_create(path: &Path) -> Result<()> {
     let mut file = std::fs::File::create(readme_path)?;
     file.write_all(new_readme.as_bytes())?;
 
-    tracing::info!(dx_src = ?TraceSrc::Dev, "Generated project at {}\n\n`cd` to your project and run `dx serve` to start developing.\nIf using Tailwind, make sure to run the Tailwind CLI.\nMore information is available in the generated `README.md`.\n\nBuild cool things! ✌️", path.display());
+    tracing::info!(dx_src = ?TraceSrc::Dev, "Generated project at {}\n\n`cd` to your project and run `dx serve` to start developing.\nMore information is available in the generated `README.md`.\n\nBuild cool things! ✌️", path.display());
 
     Ok(())
 }

+ 59 - 22
packages/cli/src/cli/mod.rs

@@ -12,6 +12,7 @@ pub(crate) mod run;
 pub(crate) mod serve;
 pub(crate) mod target;
 pub(crate) mod translate;
+pub(crate) mod update;
 pub(crate) mod verbosity;
 
 pub(crate) use build::*;
@@ -20,6 +21,7 @@ pub(crate) use target::*;
 pub(crate) use verbosity::*;
 
 use crate::{error::Result, Error, StructuredOutput};
+use clap::builder::styling::{AnsiColor, Effects, Style, Styles};
 use clap::{Parser, Subcommand};
 use html_parser::Dom;
 use once_cell::sync::Lazy;
@@ -32,9 +34,10 @@ use std::{
     process::Command,
 };
 
-/// Build, Bundle & Ship Dioxus Apps.
+/// Dioxus: build web, desktop, and mobile apps with a single codebase.
 #[derive(Parser)]
 #[clap(name = "dioxus", version = VERSION.as_str())]
+#[clap(styles = CARGO_STYLING)]
 pub(crate) struct Cli {
     #[command(subcommand)]
     pub(crate) action: Commands,
@@ -45,21 +48,29 @@ pub(crate) struct Cli {
 
 #[derive(Subcommand)]
 pub(crate) enum Commands {
+    /// Create a new Dioxus project.
+    #[clap(name = "new")]
+    New(create::Create),
+
+    /// Build, watch, and serve the project.
+    #[clap(name = "serve")]
+    Serve(serve::ServeArgs),
+
+    /// Bundle the Dioxus app into a shippable object.
+    #[clap(name = "bundle")]
+    Bundle(bundle::Bundle),
+
     /// Build the Dioxus project and all of its assets.
     #[clap(name = "build")]
     Build(build::BuildArgs),
 
-    /// Translate a source file into Dioxus code.
-    #[clap(name = "translate")]
-    Translate(translate::Translate),
-
-    /// Build, watch & serve the Dioxus project and all of its assets.
-    #[clap(name = "serve")]
-    Serve(serve::ServeArgs),
+    /// Run the project without any hotreloading.
+    #[clap(name = "run")]
+    Run(run::RunArgs),
 
-    /// Create a new project for Dioxus.
-    #[clap(name = "new")]
-    New(create::Create),
+    /// Build the assets for a specific target.
+    #[clap(name = "assets")]
+    BuildAssets(build_assets::BuildAssets),
 
     /// Init a new project for Dioxus in the current directory (by default).
     /// Will attempt to keep your project in a good state.
@@ -70,9 +81,9 @@ pub(crate) enum Commands {
     #[clap(name = "clean")]
     Clean(clean::Clean),
 
-    /// Bundle the Dioxus app into a shippable object.
-    #[clap(name = "bundle")]
-    Bundle(bundle::Bundle),
+    /// Translate a source file into Dioxus code.
+    #[clap(name = "translate")]
+    Translate(translate::Translate),
 
     /// Automatically format RSX.
     #[clap(name = "fmt")]
@@ -82,18 +93,14 @@ pub(crate) enum Commands {
     #[clap(name = "check")]
     Check(check::Check),
 
-    /// Run the project without any hotreloading
-    #[clap(name = "run")]
-    Run(run::RunArgs),
-
     /// Dioxus config file controls.
     #[clap(subcommand)]
     #[clap(name = "config")]
     Config(config::Config),
 
-    /// Build the assets for a specific target.
-    #[clap(name = "assets")]
-    BuildAssets(build_assets::BuildAssets),
+    /// Update the Dioxus CLI to the latest version.
+    #[clap(name = "self-update")]
+    SelfUpdate(update::SelfUpdate),
 }
 
 impl Display for Commands {
@@ -110,7 +117,8 @@ impl Display for Commands {
             Commands::Check(_) => write!(f, "check"),
             Commands::Bundle(_) => write!(f, "bundle"),
             Commands::Run(_) => write!(f, "run"),
-            Commands::BuildAssets(_) => write!(f, "build_assets"),
+            Commands::BuildAssets(_) => write!(f, "assets"),
+            Commands::SelfUpdate(_) => write!(f, "self-update"),
         }
     }
 }
@@ -122,3 +130,32 @@ pub(crate) static VERSION: Lazy<String> = Lazy::new(|| {
         crate::dx_build_info::GIT_COMMIT_HASH_SHORT.unwrap_or("was built without git repository")
     )
 });
+
+/// Cargo's color style
+/// [source](https://github.com/crate-ci/clap-cargo/blob/master/src/style.rs)
+pub(crate) const CARGO_STYLING: Styles = Styles::styled()
+    .header(styles::HEADER)
+    .usage(styles::USAGE)
+    .literal(styles::LITERAL)
+    .placeholder(styles::PLACEHOLDER)
+    .error(styles::ERROR)
+    .valid(styles::VALID)
+    .invalid(styles::INVALID);
+
+pub mod styles {
+    use super::*;
+    pub(crate) const HEADER: Style = AnsiColor::Green.on_default().effects(Effects::BOLD);
+    pub(crate) const USAGE: Style = AnsiColor::Green.on_default().effects(Effects::BOLD);
+    pub(crate) const LITERAL: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD);
+    pub(crate) const PLACEHOLDER: Style = AnsiColor::Cyan.on_default();
+    pub(crate) const ERROR: Style = AnsiColor::Red.on_default().effects(Effects::BOLD);
+
+    pub(crate) const VALID: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD);
+    pub(crate) const INVALID: Style = AnsiColor::Yellow.on_default().effects(Effects::BOLD);
+
+    // extra styles for styling logs
+    // we can style stuff using the ansi sequences like: "hotpatched in {GLOW_STYLE}{}{GLOW_STYLE:X}ms"
+    pub(crate) const GLOW_STYLE: Style = AnsiColor::Yellow.on_default();
+    pub(crate) const NOTE_STYLE: Style = AnsiColor::Green.on_default();
+    pub(crate) const LINK_STYLE: Style = AnsiColor::Blue.on_default();
+}

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

@@ -106,7 +106,7 @@ impl RunArgs {
                             print!("{}", message);
                         }
                         BuilderUpdate::BuildFailed { err } => {
-                            tracing::error!("Build failed: {:#?}", err);
+                            tracing::error!("Build failed: {}", err);
                         }
                         BuilderUpdate::StdoutReceived { msg } => {
                             tracing::info!("[{platform}] {msg}");

+ 247 - 0
packages/cli/src/cli/update.rs

@@ -0,0 +1,247 @@
+use super::*;
+use crate::{Result, Workspace};
+use anyhow::Context;
+use itertools::Itertools;
+use self_update::cargo_crate_version;
+
+/// Run the project with the given arguments
+///
+/// This is a shorthand for `dx serve` with interactive mode and hot-reload disabled.
+#[derive(Clone, Debug, Parser)]
+pub(crate) struct SelfUpdate {
+    /// Use the latest nightly build.
+    #[clap(long, default_value = "false")]
+    pub nightly: bool,
+
+    /// Specify a version to install.
+    #[clap(long)]
+    pub version: Option<String>,
+
+    /// Install the update.
+    #[clap(long, default_value = "true")]
+    pub install: bool,
+
+    /// List available versions.
+    #[clap(long, default_value = "false")]
+    pub list: bool,
+
+    /// Force the update even if the current version is up to date.
+    #[clap(long, default_value = "false")]
+    pub force: bool,
+}
+
+impl SelfUpdate {
+    pub async fn self_update(self) -> Result<StructuredOutput> {
+        tokio::task::spawn_blocking(move || {
+            let start = std::time::Instant::now();
+            if self.list {
+                let res = self_update::backends::github::Update::configure()
+                    .repo_owner("dioxuslabs")
+                    .repo_name("dioxus")
+                    .bin_name("dx")
+                    .current_version(cargo_crate_version!())
+                    .build()
+                    .unwrap()
+                    .get_latest_releases(cargo_crate_version!())
+                    .context("Failed to fetch latest version")?;
+
+                if res.is_empty() {
+                    tracing::info!("Your version {} is up to date!", cargo_crate_version!());
+                } else {
+                    tracing::info!("Your version {} is out of date!", cargo_crate_version!());
+                    tracing::info!(
+                        "Available versions: [{}]",
+                        res.iter()
+                            .map(|r| r.version.clone())
+                            .collect::<Vec<_>>()
+                            .join(", ")
+                    );
+                }
+
+                return Ok(StructuredOutput::Success);
+            }
+
+            let repo = self_update::backends::github::Update::configure()
+                .repo_owner("dioxuslabs")
+                .repo_name("dioxus")
+                .bin_name("dx")
+                .current_version(cargo_crate_version!())
+                .build()
+                .unwrap();
+
+            let force = self.force || self.version.is_some();
+            let latest = match self.version {
+                Some(version) => repo
+                    .get_release_version(&version)
+                    .context("Failed to fetch release by tag")?,
+                None => repo
+                    .get_latest_release()
+                    .context("Failed to fetch latest version")?,
+            };
+
+            if latest.version == cargo_crate_version!() && !force {
+                tracing::info!("Your version {} is up to date!", cargo_crate_version!());
+                return Ok(StructuredOutput::Success);
+            }
+
+            tracing::info!("Your version is out of date!");
+            tracing::info!("- Yours:  {}", cargo_crate_version!());
+            tracing::info!("- Latest: {}", latest.version);
+
+            let cur_arch = if cfg!(target_arch = "x86_64") {
+                "x86_64"
+            } else if cfg!(target_arch = "aarch64") {
+                "aarch64"
+            } else {
+                return Err(Error::Unique("Unsupported architecture".to_string()));
+            };
+
+            let cur_os = if cfg!(target_os = "windows") {
+                "windows"
+            } else if cfg!(target_os = "linux") {
+                "linux"
+            } else if cfg!(target_os = "macos") {
+                "darwin"
+            } else {
+                return Err(Error::Unique("Unsupported OS".to_string()));
+            };
+
+            let zip_ext = "zip";
+
+            tracing::debug!("Available assets: {:?}", latest.assets);
+
+            let asset = latest
+                .assets
+                .iter()
+                .find(|a| {
+                    a.name.contains(cur_os)
+                        && a.name.contains(cur_arch)
+                        && a.name.ends_with(zip_ext)
+                })
+                .ok_or_else(|| Error::Unique("No suitable release found found".to_string()))?;
+
+            let install_dir = Workspace::dioxus_home_dir().join("self-update");
+            std::fs::create_dir_all(&install_dir).context("Failed to create install directory")?;
+
+            tracing::info!("Downloading update from Github");
+            tracing::debug!("Download URL: {}", asset.download_url);
+            let body = latest.body.unwrap_or_default();
+            let brief = vec![
+                latest.name.to_string(),
+                "".to_string(),
+                latest.date.to_string(),
+                asset.download_url.to_string(),
+                "".to_string(),
+            ]
+            .into_iter()
+            .chain(body.lines().map(ToString::to_string).take(7))
+            .chain(std::iter::once(" ...".to_string()))
+            .map(|line| format!("                | {line}"))
+            .join("\n");
+
+            tracing::info!("{}", brief.trim());
+
+            let archive_path = install_dir.join(&asset.name);
+            _ = std::fs::remove_file(&archive_path).ok();
+            let archive_file = std::fs::File::create(&archive_path)?;
+            let download_url = asset.download_url.clone();
+            self_update::Download::from_url(&download_url)
+                .set_header(
+                    hyper::http::header::ACCEPT,
+                    "application/octet-stream".parse().unwrap(),
+                )
+                .download_to(archive_file)
+                .context("Failed to download update")?;
+
+            let install_dir = install_dir.join("dx");
+            _ = std::fs::remove_dir_all(&install_dir);
+            self_update::Extract::from_source(&archive_path)
+                .extract_into(&install_dir)
+                .context("Failed to extract update")?;
+
+            let executable = install_dir.join("dx");
+            if !executable.exists() {
+                return Err(Error::Unique(format!(
+                    "Executable not found in {}",
+                    install_dir.display()
+                )));
+            }
+
+            tracing::info!(
+                "Successfully downloaded update in {}ms! 👍",
+                start.elapsed().as_millis()
+            );
+
+            if self.install {
+                tracing::info!(
+                    "Installing dx v{} to {}",
+                    latest.version,
+                    std::env::current_exe()?.display()
+                );
+
+                if !self.force {
+                    tracing::warn!("Continue? (y/n)");
+                    print!("                > ");
+                    std::io::stdout()
+                        .flush()
+                        .context("Failed to flush stdout")?;
+                    let mut input = String::new();
+                    std::io::stdin()
+                        .read_line(&mut input)
+                        .context("Failed to read input")?;
+                    if !input.trim().to_ascii_lowercase().starts_with('y') {
+                        tracing::info!("Aborting update");
+                        return Ok(StructuredOutput::Success);
+                    }
+                }
+
+                self_update::self_replace::self_replace(executable)?;
+                let time_taken = start.elapsed().as_millis();
+                tracing::info!("Done in {} ms! 💫", time_taken)
+            } else {
+                tracing::info!("Update downloaded to {}", install_dir.display());
+                tracing::info!("Run `dx self-update --install` to install the update");
+            }
+
+            Ok(StructuredOutput::Success)
+        })
+        .await
+        .context("Failed to run self-update")?
+    }
+}
+
+/// Check against the github release list to see if the currently released `dx` version is
+/// more up-to-date than our own.
+///
+/// We only toss out this warning once and then save to the settings file to ignore this version
+/// in the future.
+pub fn log_if_cli_could_update() {
+    tokio::task::spawn_blocking(|| {
+        let release = self_update::backends::github::Update::configure()
+            .repo_owner("dioxuslabs")
+            .repo_name("dioxus")
+            .bin_name("dx")
+            .current_version(cargo_crate_version!())
+            .build()
+            .unwrap()
+            .get_latest_release();
+
+        if let Ok(release) = release {
+            let old = krates::semver::Version::parse(cargo_crate_version!());
+            let new = krates::semver::Version::parse(&release.version);
+
+            if let (Ok(old), Ok(new)) = (old, new) {
+                if old < new {
+                    _ = crate::CliSettings::modify_settings(|f| {
+                        let ignored = f.ignore_version_update.as_deref().unwrap_or_default();
+                        if release.version != ignored {
+                            use crate::styles::GLOW_STYLE;
+                            tracing::warn!("A new dx version is available: {new}! Run {GLOW_STYLE}dx self-update{GLOW_STYLE:#} to update.");
+                            f.ignore_version_update = Some(new.to_string());
+                        }
+                    });
+                }
+            }
+        }
+    });
+}

+ 23 - 5
packages/cli/src/config/app.rs

@@ -3,11 +3,6 @@ use std::path::PathBuf;
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub(crate) struct ApplicationConfig {
-    pub(crate) asset_dir: Option<PathBuf>,
-
-    #[serde(default)]
-    pub(crate) sub_package: Option<String>,
-
     #[serde(default)]
     pub(crate) out_dir: Option<PathBuf>,
 
@@ -16,4 +11,27 @@ pub(crate) struct ApplicationConfig {
 
     #[serde(default)]
     pub(crate) tailwind_output: Option<PathBuf>,
+
+    /// Use this file for the info.plist associated with the iOS app.
+    /// `dx` will merge any required settings into this file required to build the app
+    #[serde(default)]
+    pub(crate) ios_info_plist: Option<PathBuf>,
+
+    /// Use this file for the info.plist associated with the macOS app.
+    /// `dx` will merge any required settings into this file required to build the app
+    #[serde(default)]
+    pub(crate) macos_info_plist: Option<PathBuf>,
+
+    /// Use this file for the entitlements.plist associated with the iOS app.
+    #[serde(default)]
+    pub(crate) ios_entitlements: Option<PathBuf>,
+
+    /// Use this file for the entitlements.plist associated with the macOS app.
+    #[serde(default)]
+    pub(crate) macos_entitlements: Option<PathBuf>,
+
+    /// Use this file for the AndroidManifest.xml associated with the Android app.
+    /// `dx` will merge any required settings into this file required to build the app
+    #[serde(default)]
+    pub(crate) android_manifest: Option<PathBuf>,
 }

+ 0 - 5
packages/cli/src/config/desktop.rs

@@ -1,5 +0,0 @@
-use serde::{Deserialize, Serialize};
-
-/// Represents configuration items for the desktop platform.
-#[derive(Debug, Default, Clone, Serialize, Deserialize)]
-pub(crate) struct DesktopConfig {}

+ 5 - 6
packages/cli/src/config/dioxus_config.rs

@@ -8,9 +8,6 @@ pub(crate) struct DioxusConfig {
     #[serde(default)]
     pub(crate) web: WebConfig,
 
-    #[serde(default)]
-    pub(crate) desktop: DesktopConfig,
-
     #[serde(default)]
     pub(crate) bundle: BundleConfig,
 }
@@ -19,11 +16,14 @@ impl Default for DioxusConfig {
     fn default() -> Self {
         Self {
             application: ApplicationConfig {
-                asset_dir: None,
-                sub_package: None,
                 out_dir: None,
                 tailwind_input: None,
                 tailwind_output: None,
+                ios_info_plist: None,
+                android_manifest: None,
+                macos_info_plist: None,
+                ios_entitlements: None,
+                macos_entitlements: None,
             },
             web: WebConfig {
                 app: WebAppConfig {
@@ -49,7 +49,6 @@ impl Default for DioxusConfig {
                 pre_compress: true,
                 wasm_opt: Default::default(),
             },
-            desktop: DesktopConfig::default(),
             bundle: BundleConfig::default(),
         }
     }

+ 0 - 2
packages/cli/src/config/mod.rs

@@ -1,13 +1,11 @@
 mod app;
 mod bundle;
-mod desktop;
 mod dioxus_config;
 mod serve;
 mod web;
 
 pub(crate) use app::*;
 pub(crate) use bundle::*;
-pub(crate) use desktop::*;
 pub(crate) use dioxus_config::*;
 pub(crate) use serve::*;
 pub(crate) use web::*;

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

@@ -49,7 +49,6 @@ async fn main() {
     }
 
     let args = TraceController::initialize();
-
     let result = match args.action {
         Commands::Translate(opts) => opts.translate(),
         Commands::New(opts) => opts.create(),
@@ -63,6 +62,7 @@ async fn main() {
         Commands::Bundle(opts) => opts.bundle().await,
         Commands::Run(opts) => opts.run().await,
         Commands::BuildAssets(opts) => opts.run().await,
+        Commands::SelfUpdate(opts) => opts.self_update().await,
     };
 
     // Provide a structured output for third party tools that can consume the output of the CLI

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

@@ -1,4 +1,7 @@
-use crate::{AppBuilder, BuildId, BuildMode, BuilderUpdate, Result, ServeArgs, TraceController};
+use crate::{
+    styles::{GLOW_STYLE, LINK_STYLE},
+    AppBuilder, BuildId, BuildMode, BuilderUpdate, Result, ServeArgs, TraceController,
+};
 
 mod ansi_buffer;
 mod output;
@@ -34,26 +37,25 @@ pub(crate) use update::*;
 /// - I want us to be able to detect a `server_fn` in the project and then upgrade from a static server
 ///   to a dynamic one on the fly.
 pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> Result<()> {
-    // Load the args into a plan, resolving all tooling, build dirs, arguments, decoding the multi-target, etc
-    let mut builder = AppServer::start(args).await?;
-    let mut devserver = WebServer::start(&builder)?;
-    let mut screen = Output::start(builder.interactive).await?;
-
     // This is our default splash screen. We might want to make this a fancier splash screen in the future
     // Also, these commands might not be the most important, but it's all we've got enabled right now
     tracing::info!(
         r#"-----------------------------------------------------------------
-                Serving your Dioxus app: {} 🚀
-                • Press `ctrl+c` to exit the server
-                • Press `r` to rebuild the app
-                • Press `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
+                Serving your Dioxus app! 🚀
+                • Press {GLOW_STYLE}`ctrl+c`{GLOW_STYLE:#} to exit the server
+                • Press {GLOW_STYLE}`r`{GLOW_STYLE:#} to rebuild the app
+                • Press {GLOW_STYLE}`p`{GLOW_STYLE:#} to toggle automatic rebuilds
+                • Press {GLOW_STYLE}`v`{GLOW_STYLE:#} to toggle verbose logging
+                • Press {GLOW_STYLE}`/`{GLOW_STYLE:#} for more commands and shortcuts
+                Learn more at {LINK_STYLE}https://dioxuslabs.com/learn/0.7/getting_started{LINK_STYLE:#}
                ----------------------------------------------------------------"#,
-        builder.app_name()
     );
 
+    // Load the args into a plan, resolving all tooling, build dirs, arguments, decoding the multi-target, etc
+    let mut builder = AppServer::start(args).await?;
+    let mut devserver = WebServer::start(&builder)?;
+    let mut screen = Output::start(builder.interactive).await?;
+
     loop {
         // Draw the state of the server to the screen
         screen.render(&builder, &devserver);
@@ -89,7 +91,6 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) ->
             // Run the server in the background
             // Waiting for updates here lets us tap into when clients are added/removed
             ServeUpdate::NewConnection { id, aslr_reference } => {
-                // Send the client
                 devserver
                     .send_hotreload(builder.applied_hot_reload_changes(BuildId::CLIENT))
                     .await;
@@ -128,7 +129,7 @@ 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!("Build failed: {}", err);
                     }
                     BuilderUpdate::BuildReady { bundle } => match bundle.mode {
                         BuildMode::Thin { ref cache, .. } => {
@@ -204,7 +205,6 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) ->
             ServeUpdate::Exit { error } => {
                 _ = builder.cleanup_all().await;
                 _ = devserver.shutdown().await;
-                _ = screen.shutdown();
 
                 match error {
                     Some(err) => return Err(anyhow::anyhow!("{}", err).into()),

+ 16 - 11
packages/cli/src/serve/output.rs

@@ -163,13 +163,6 @@ impl Output {
         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<()> {
-        Self::remote_shutdown(self.interactive)?;
-        Ok(())
-    }
-
     pub(crate) fn remote_shutdown(interactive: bool) -> io::Result<()> {
         if interactive {
             stdout()
@@ -966,11 +959,23 @@ impl Output {
                                 " ".repeat(3usize.saturating_sub(log.source.to_string().len()))
                         ))
                         .style(match log.source {
-                            TraceSrc::App(_platform) => Style::new().blue(),
-                            TraceSrc::Dev => Style::new().magenta(),
-                            TraceSrc::Build => Style::new().yellow(),
-                            TraceSrc::Bundle => Style::new().magenta(),
+                            TraceSrc::App(_platform) => match log.level {
+                                Level::ERROR => Style::new().red(),
+                                Level::WARN => Style::new().yellow(),
+                                Level::INFO => Style::new().magenta(),
+                                Level::DEBUG => Style::new().magenta(),
+                                Level::TRACE => Style::new().magenta(),
+                            },
+                            TraceSrc::Dev => match log.level {
+                                Level::ERROR => Style::new().red(),
+                                Level::WARN => Style::new().yellow(),
+                                Level::INFO => Style::new().blue(),
+                                Level::DEBUG => Style::new().blue(),
+                                Level::TRACE => Style::new().blue(),
+                            },
                             TraceSrc::Cargo => Style::new().yellow(),
+                            TraceSrc::Build => Style::new().blue(),
+                            TraceSrc::Bundle => Style::new().blue(),
                             TraceSrc::Unknown => Style::new().gray(),
                         }),
                     );

+ 19 - 6
packages/cli/src/serve/runner.rs

@@ -155,10 +155,12 @@ impl AppServer {
             client.build.package_manifest_dir(),
             client.build.config.application.tailwind_input.clone(),
             client.build.config.application.tailwind_output.clone(),
-        )
-        .await?;
+        );
+
+        _ = client.build.start_simulators().await;
 
-        tracing::debug!("Proxied port: {:?}", proxied_port);
+        // Encourage the user to update to a new dx version
+        crate::update::log_if_cli_could_update();
 
         // Create the runner
         let mut runner = Self {
@@ -446,7 +448,13 @@ impl AppServer {
             // Also make sure the builder isn't busy since that might cause issues with hotreloads
             // https://github.com/DioxusLabs/dioxus/issues/3361
             if !msg.is_empty() && self.client.can_receive_hotreloads() {
-                tracing::info!(dx_src = ?TraceSrc::Dev, "Hotreloading: {}", file);
+                use crate::styles::NOTE_STYLE;
+                tracing::info!(dx_src = ?TraceSrc::Dev, "Hotreloading: {NOTE_STYLE}{}{NOTE_STYLE:#}", file);
+
+                if !server.has_hotreload_sockets() && self.client.build.platform != Platform::Web {
+                    tracing::warn!("No clients to hotreload - try reloading the app!");
+                }
+
                 server.send_hotreload(msg).await;
             } else {
                 tracing::debug!(dx_src = ?TraceSrc::Dev, "Ignoring file change: {}", file);
@@ -473,6 +481,8 @@ impl AppServer {
         let should_open = self.client.stage == BuildStage::Success
             && (self.server.as_ref().map(|s| s.stage == BuildStage::Success)).unwrap_or(true);
 
+        use crate::cli::styles::GLOW_STYLE;
+
         if should_open {
             let time_taken = artifacts
                 .time_end
@@ -481,11 +491,14 @@ impl AppServer {
 
             if self.client.builds_opened == 0 {
                 tracing::info!(
-                    "Build completed successfully in {:?}ms, launching app! 💫",
+                    "Build completed successfully in {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}, launching app! 💫",
                     time_taken.as_millis()
                 );
             } else {
-                tracing::info!("Build completed in {:?}ms", time_taken.as_millis());
+                tracing::info!(
+                    "Build completed in {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}",
+                    time_taken.as_millis()
+                );
             }
 
             let open_browser = self.client.builds_opened == 0 && self.open_browser;

+ 4 - 0
packages/cli/src/serve/server.rs

@@ -264,6 +264,10 @@ impl WebServer {
         }
     }
 
+    pub(crate) fn has_hotreload_sockets(&self) -> bool {
+        !self.hot_reload_sockets.is_empty()
+    }
+
     /// Sends hot reloadable changes to all clients.
     pub(crate) async fn send_hotreload(&mut self, reload: HotReloadMsg) {
         if reload.is_empty() {

+ 2 - 0
packages/cli/src/settings.rs

@@ -26,6 +26,8 @@ pub(crate) struct CliSettings {
     pub(crate) wsl_file_poll_interval: Option<u16>,
     /// Use tooling from path rather than downloading them.
     pub(crate) no_downloads: Option<bool>,
+    /// Ignore updates for this version
+    pub(crate) ignore_version_update: Option<String>,
 }
 
 impl CliSettings {

+ 11 - 6
packages/cli/src/tailwind.rs

@@ -19,12 +19,12 @@ impl TailwindCli {
         Self { version }
     }
 
-    pub(crate) async fn serve(
+    pub(crate) fn serve(
         manifest_dir: PathBuf,
         input_path: Option<PathBuf>,
         output_path: Option<PathBuf>,
-    ) -> Result<tokio::task::JoinHandle<Result<()>>> {
-        Ok(tokio::spawn(async move {
+    ) -> tokio::task::JoinHandle<Result<()>> {
+        tokio::spawn(async move {
             let Some(tailwind) = Self::autodetect(&manifest_dir) else {
                 return Ok(());
             };
@@ -38,12 +38,16 @@ impl TailwindCli {
             proc.wait_with_output().await?;
 
             Ok(())
-        }))
+        })
     }
 
     /// Use the correct tailwind version based on the manifest directory.
+    ///
     /// - If `tailwind.config.js` or `tailwind.config.ts` exists, use v3.
     /// - If `tailwind.css` exists, use v4.
+    ///
+    /// Note that v3 still uses the tailwind.css file, but usually the accompanying js file indicates
+    /// that the project is using v3.
     pub(crate) fn autodetect(manifest_dir: &Path) -> Option<Self> {
         if manifest_dir.join("tailwind.config.js").exists() {
             return Some(Self::v3());
@@ -92,8 +96,9 @@ impl TailwindCli {
             .arg("--output")
             .arg(output_path)
             .arg("--watch")
-            .stdout(Stdio::piped())
-            .stderr(Stdio::piped())
+            .stdin(Stdio::null())
+            .stdout(Stdio::null())
+            .stderr(Stdio::null())
             .spawn()?;
 
         Ok(proc)

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

@@ -156,7 +156,7 @@ impl WasmBindgen {
             .expect("input_path should be valid utf8");
         args.push(input_path);
 
-        tracing::debug!("wasm-bindgen args: {:#?}", args);
+        tracing::debug!("wasm-bindgen: {:#?}", args);
 
         // Run bindgen
         let output = Command::new(binary).args(args).output().await?;

+ 69 - 5
packages/cli/src/workspace.rs

@@ -3,11 +3,11 @@ use crate::Result;
 use crate::{config::DioxusConfig, AndroidTools};
 use anyhow::Context;
 use ignore::gitignore::Gitignore;
-use krates::KrateDetails;
+use krates::{semver::Version, KrateDetails, LockOptions};
 use krates::{Cmd, Krates, NodeId};
-use std::path::Path;
 use std::path::PathBuf;
 use std::sync::Arc;
+use std::{collections::HashSet, path::Path};
 use target_lexicon::Triple;
 use tokio::process::Command;
 
@@ -34,9 +34,12 @@ impl Workspace {
             return Ok(ws.clone());
         }
 
-        tracing::debug!("Loading workspace!");
-
-        let cmd = Cmd::new();
+        let mut cmd = Cmd::new();
+        cmd.lock_opts(LockOptions {
+            offline: true,
+            frozen: false,
+            locked: false,
+        });
         let mut builder = krates::Builder::new();
         builder.workspace(true);
         let krates = builder
@@ -79,6 +82,23 @@ impl Workspace {
             android_tools,
         });
 
+        tracing::debug!(
+            r#"Initialized workspace:
+               • sysroot: {sysroot}
+               • rustc version: {rustc_version}
+               • workspace root: {workspace_root}
+               • dioxus versions: [{dioxus_versions:?}]"#,
+            sysroot = workspace.sysroot.display(),
+            rustc_version = workspace.rustc_version,
+            workspace_root = workspace.workspace_root().display(),
+            dioxus_versions = workspace
+                .dioxus_versions()
+                .iter()
+                .map(|v| v.to_string())
+                .collect::<Vec<_>>()
+                .join(", ")
+        );
+
         lock.replace(workspace.clone());
 
         Ok(workspace)
@@ -116,6 +136,50 @@ impl Workspace {
         false
     }
 
+    pub fn check_dioxus_version_against_cli(&self) {
+        let dx_semver = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
+        let dioxus_versions = self.dioxus_versions();
+
+        tracing::trace!("dx version: {}", dx_semver);
+        tracing::trace!("dioxus versions: {:?}", dioxus_versions);
+
+        // if there are no dioxus versions in the workspace, we don't need to check anything
+        // dx is meant to be compatible with non-dioxus projects too.
+        if dioxus_versions.is_empty() {
+            return;
+        }
+
+        let min = dioxus_versions.iter().min().unwrap();
+        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 {
+            tracing::error!(
+                r#"🚫dx and dioxus versions are incompatible!
+                  • dx version: {dx_semver}
+                  • dioxus versions: [{}]"#,
+                dioxus_versions
+                    .iter()
+                    .map(|v| v.to_string())
+                    .collect::<Vec<_>>()
+                    .join(", ")
+            );
+        }
+    }
+
+    /// Get all the versions of dioxus in the workspace
+    pub fn dioxus_versions(&self) -> Vec<Version> {
+        let mut versions = HashSet::new();
+        for krate in self.krates.krates() {
+            if krate.name == "dioxus" {
+                versions.insert(krate.version.clone());
+            }
+        }
+        let mut versions = versions.into_iter().collect::<Vec<_>>();
+        versions.sort();
+        versions
+    }
+
     #[allow(unused)]
     pub fn rust_lld(&self) -> PathBuf {
         self.sysroot

+ 0 - 3
packages/core-macro/src/component.rs

@@ -98,7 +98,6 @@ impl ComponentBody {
 
         let Generics { where_clause, .. } = generics;
         let (_, impl_generics, _) = generics.split_for_impl();
-        let generics_turbofish = impl_generics.as_turbofish();
 
         // We generate a struct with the same name as the component but called `Props`
         let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
@@ -147,8 +146,6 @@ impl ComponentBody {
             #[allow(non_snake_case)]
             #vis fn #fn_ident #generics (#emit_props) #fn_output #where_clause {
                 {
-                    // In debug mode we can detect if the user is calling the component like a function
-                    dioxus_core::internal::verify_component_called_as_component(#fn_ident #generics_turbofish);
                     #body
                 }
             }

+ 0 - 2
packages/core/src/lib.rs

@@ -33,8 +33,6 @@ mod hotreload_utils;
 /// Items exported from this module are used in macros and should not be used directly.
 #[doc(hidden)]
 pub mod internal {
-    pub use crate::properties::verify_component_called_as_component;
-
     #[doc(hidden)]
     pub use crate::hotreload_utils::{
         DynamicLiteralPool, DynamicValuePool, FmtSegment, FmtedSegments, HotReloadAttributeValue,

+ 0 - 29
packages/core/src/properties.rs

@@ -114,35 +114,6 @@ where
     P::builder()
 }
 
-/// A warning that will trigger if a component is called as a function
-#[warnings::warning]
-pub(crate) fn component_called_as_function<C: ComponentFunction<P, M>, P, M>(_: C) {
-    // We trim WithOwner from the end of the type name for component with a builder that include a special owner which may not match the function name directly
-    let type_name = std::any::type_name::<C>();
-    let component_name = Runtime::with(|rt| {
-        current_scope_id()
-            .ok()
-            .and_then(|id| rt.get_state(id).map(|scope| scope.name))
-    })
-    .ok()
-    .flatten();
-
-    // If we are in a component, and the type name is the same as the active component name, then we can just return
-    if component_name == Some(type_name) {
-        return;
-    }
-
-    // Otherwise the component was called like a function, so we should log an error
-    tracing::error!("It looks like you called the component {type_name} like a function instead of a component. Components should be called with braces like `{type_name} {{ prop: value }}` instead of as a function");
-}
-
-/// Make sure that this component is currently running as a component, not a function call
-#[doc(hidden)]
-#[allow(clippy::no_effect)]
-pub fn verify_component_called_as_component<C: ComponentFunction<P, M>, P, M>(component: C) {
-    component_called_as_function(component);
-}
-
 /// Any component that implements the `ComponentFn` trait can be used as a component.
 ///
 /// This trait is automatically implemented for functions that are in one of the following forms:

+ 1 - 8
packages/core/src/virtual_dom.rs

@@ -239,14 +239,7 @@ impl VirtualDom {
     ///
     /// Note: the VirtualDom is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it.
     pub fn new(app: fn() -> Element) -> Self {
-        Self::new_with_props(
-            move || {
-                use warnings::Warning;
-                // The root props don't come from a vcomponent so we need to manually rerun them sometimes
-                crate::properties::component_called_as_function::allow(app)
-            },
-            (),
-        )
+        Self::new_with_props(app, ())
     }
 
     /// Create a new VirtualDom with the given properties for the root component.

+ 1 - 1
packages/desktop/Cargo.toml

@@ -50,7 +50,7 @@ tao = { workspace = true, features = ["rwh_05"] }
 once_cell = { workspace = true }
 dioxus-history = { workspace = true }
 base64 = { workspace = true }
-
+libc = "0.2.170"
 
 [target.'cfg(unix)'.dependencies]
 signal-hook = "0.3.17"

+ 91 - 13
packages/desktop/src/app.rs

@@ -7,7 +7,8 @@ use crate::{
     shortcut::ShortcutRegistry,
     webview::WebviewInstance,
 };
-use dioxus_core::{ElementId, VirtualDom};
+use dioxus_core::{ElementId, ScopeId, VirtualDom};
+use dioxus_history::History;
 use dioxus_html::PlatformEventData;
 use std::{
     any::Any,
@@ -15,6 +16,7 @@ use std::{
     collections::HashMap,
     rc::Rc,
     sync::Arc,
+    time::Duration,
 };
 use tao::{
     dpi::PhysicalSize,
@@ -168,12 +170,10 @@ impl App {
 
     #[cfg(all(feature = "devtools", debug_assertions))]
     pub fn connect_hotreload(&self) {
-        if let Some(endpoint) = dioxus_cli_config::devserver_ws_endpoint() {
-            let proxy = self.shared.proxy.clone();
-            dioxus_devtools::connect(endpoint, move |msg| {
-                _ = proxy.send_event(UserWindowEvent::HotReloadEvent(msg));
-            })
-        }
+        let proxy = self.shared.proxy.clone();
+        dioxus_devtools::connect(move |msg| {
+            _ = proxy.send_event(UserWindowEvent::HotReloadEvent(msg));
+        })
     }
 
     pub fn handle_new_window(&mut self) {
@@ -322,8 +322,14 @@ impl App {
 
     #[cfg(all(feature = "devtools", debug_assertions))]
     pub fn handle_hot_reload_msg(&mut self, msg: dioxus_devtools::DevserverMsg) {
+        use std::time::Duration;
+
         use dioxus_devtools::DevserverMsg;
 
+        // Amount of time that toats should be displayed.
+        const TOAST_TIMEOUT: Duration = Duration::from_secs(2);
+        const TOAST_TIMEOUT_LONG: Duration = Duration::from_secs(3600); // Duration::MAX is too long for JS.
+
         match msg {
             DevserverMsg::HotReload(hr_msg) => {
                 for webview in self.webviews.values_mut() {
@@ -343,13 +349,49 @@ impl App {
                         webview.kick_stylsheets();
                     }
                 }
+
+                if hr_msg.jump_table.is_some()
+                    && hr_msg.for_build_id == Some(dioxus_cli_config::build_id())
+                {
+                    self.send_toast_to_all(
+                        "Hot-patch success!",
+                        &format!("App successfully patched in {} ms", hr_msg.ms_elapsed),
+                        "success",
+                        TOAST_TIMEOUT,
+                        false,
+                    );
+                }
             }
-            DevserverMsg::FullReloadCommand
-            | DevserverMsg::FullReloadStart
-            | DevserverMsg::FullReloadFailed => {
-                // usually only web gets this message - what are we supposed to do?
-                // Maybe we could just binary patch ourselves in place without losing window state?
+            DevserverMsg::FullReloadCommand => {
+                self.send_toast_to_all(
+                    "Successfully rebuilt.",
+                    "Your app was rebuilt successfully and without error.",
+                    "success",
+                    TOAST_TIMEOUT,
+                    true,
+                );
             }
+            DevserverMsg::FullReloadStart => self.send_toast_to_all(
+                "Your app is being rebuilt.",
+                "A non-hot-reloadable change occurred and we must rebuild.",
+                "info",
+                TOAST_TIMEOUT_LONG,
+                false,
+            ),
+            DevserverMsg::FullReloadFailed => self.send_toast_to_all(
+                "Oops! The build failed.",
+                "We tried to rebuild your app, but something went wrong.",
+                "error",
+                TOAST_TIMEOUT_LONG,
+                false,
+            ),
+            DevserverMsg::HotPatchStart => self.send_toast_to_all(
+                "Hot-patching app...",
+                "Hot-patching modified Rust code.",
+                "info",
+                TOAST_TIMEOUT_LONG,
+                false,
+            ),
             DevserverMsg::Shutdown => {
                 self.control_flow = ControlFlow::Exit;
             }
@@ -357,6 +399,20 @@ impl App {
         }
     }
 
+    #[cfg(all(feature = "devtools", debug_assertions))]
+    fn send_toast_to_all(
+        &self,
+        header_text: &str,
+        message: &str,
+        level: &str,
+        duration: Duration,
+        after_reload: bool,
+    ) {
+        for webview in self.webviews.values() {
+            webview.show_toast(header_text, message, level, duration, after_reload);
+        }
+    }
+
     pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) {
         let Ok(file_dialog) = serde_json::from_value::<FileDialogRequest>(msg.params()) else {
             return;
@@ -462,6 +518,9 @@ impl App {
 
     #[cfg(debug_assertions)]
     fn persist_window_state(&self) {
+        use dioxus_core::ScopeId;
+        use dioxus_history::History;
+
         if let Some(webview) = self.webviews.values().next() {
             let window = &webview.desktop_context.window;
 
@@ -491,12 +550,20 @@ impl App {
                 return;
             };
 
+            let url = webview.dom.in_runtime(|| {
+                ScopeId::ROOT
+                    .consume_context::<Rc<dyn History>>()
+                    .unwrap()
+                    .current_route()
+            });
+
             let state = PreservedWindowState {
                 x,
                 y,
                 width: size.width.max(200),
                 height: size.height.saturating_sub(adjustment).max(200),
                 monitor: monitor_name.to_string(),
+                url: Some(url),
             };
 
             // Yes... I know... we're loading a file that might not be ours... but it's a debug feature
@@ -540,6 +607,16 @@ impl App {
                 if explicit_inner_size.is_none() {
                     window.set_inner_size(tao::dpi::PhysicalSize::new(size.0, size.1));
                 }
+
+                // Set the url if it exists
+                webview.dom.in_runtime(|| {
+                    if let Some(url) = state.url {
+                        ScopeId::ROOT
+                            .consume_context::<Rc<dyn History>>()
+                            .unwrap()
+                            .replace(url);
+                    }
+                })
             }
         }
     }
@@ -563,7 +640,7 @@ impl App {
                         }
 
                         // give it a moment for the event to be processed
-                        std::thread::sleep(std::time::Duration::from_secs(1));
+                        std::thread::sleep(std::time::Duration::from_millis(100));
                     }
                 }
             });
@@ -578,6 +655,7 @@ struct PreservedWindowState {
     width: u32,
     height: u32,
     monitor: String,
+    url: Option<String>,
 }
 
 /// Hide the last window when using LastWindowHides.

+ 184 - 0
packages/desktop/src/assets/dev.index.html

@@ -0,0 +1,184 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>Dioxus app</title>
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+        <style>
+            /* Inter Font */
+            @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
+
+            #dx-toast-template {
+                display: none;
+                visibility: hidden;
+            }
+
+            .dx-toast {
+                position: absolute;
+                top: 10px;
+                right: 0;
+                padding-right: 10px;
+                user-select: none;
+                /* transition: transform 0.2s ease; */
+                z-index: 2147483647;
+            }
+
+            .dx-toast .dx-toast-inner {
+                /* transition: right 0.2s ease-out; */
+                position: fixed;
+
+                background-color: #181B20;
+                color: #ffffff;
+                font-family: "Inter", sans-serif;
+
+                display: grid;
+                grid-template-columns: auto auto;
+                max-width: 400px;
+                min-height: 56px;
+                border-radius: 5px;
+            }
+
+            .dx-toast .dx-toast-inner {
+                cursor: pointer;
+                margin-right: 10px;
+            }
+
+            .dx-toast .dx-toast-level-bar-container {
+                height: 100%;
+                width: 6px;
+            }
+
+            .dx-toast .dx-toast-level-bar-container .dx-toast-level-bar {
+                width: 100%;
+                height: 100%;
+                border-radius: 5px 0px 0px 5px;
+            }
+
+            .dx-toast .dx-toast-content {
+                padding: 8px;
+            }
+
+            .dx-toast .dx-toast-header {
+                display: flex;
+                flex-direction: row;
+                justify-content: start;
+                align-items: end;
+                margin-bottom: 10px;
+            }
+
+            .dx-toast .dx-toast-header>svg {
+                height: 18px;
+                margin-right: 5px;
+            }
+
+            .dx-toast .dx-toast-header .dx-toast-header-text {
+                font-size: 14px;
+                font-weight: 700;
+                padding: 0;
+                margin: 0;
+            }
+
+            .dx-toast .dx-toast-msg {
+                font-size: 11px;
+                font-weight: 400;
+                padding: 0;
+                margin: 0;
+            }
+
+            .dx-toast-level-bar.info {
+                background-color: #428EFF;
+            }
+
+            .dx-toast-level-bar.success {
+                background-color: #42FF65;
+            }
+
+            .dx-toast-level-bar.error {
+                background-color: #FF4242;
+            }
+        </style>
+        <script>
+            const STORAGE_KEY = "SCHEDULED-DX-TOAST";
+            let currentTimeout = null;
+            let currentToastId = 0;
+
+            // Show a toast, removing the previous one.
+            function showDXToast(headerText, message, progressLevel, durationMs) {
+                document.getElementById("__dx-toast-decor").className = `dx-toast-level-bar ${progressLevel}`;
+                document.getElementById("__dx-toast-text").innerText = headerText;
+                document.getElementById("__dx-toast-msg").innerText = message;
+                document.getElementById("__dx-toast-inner").style.right = "0";
+                document.getElementById("__dx-toast").addEventListener("click", closeDXToast);
+
+
+                // Wait a bit of time so animation plays correctly.
+                setTimeout(
+                    () => {
+                        let ourToastId = currentToastId;
+                        currentTimeout = setTimeout(() => {
+                            if (ourToastId == currentToastId) {
+                                closeDXToast();
+                            }
+                        }, durationMs);
+                    },
+                    100
+                );
+
+                currentToastId += 1;
+            }
+
+            // Schedule a toast to be displayed after reload.
+            function scheduleDXToast(headerText, message, level, durationMs) {
+                let data = {
+                    headerText,
+                    message,
+                    level,
+                    durationMs,
+                };
+
+                let jsonData = JSON.stringify(data);
+                sessionStorage.setItem(STORAGE_KEY, jsonData);
+            }
+
+            // Close the current toast.
+            function closeDXToast() {
+                document.getElementById("__dx-toast-inner").style.right = "-1000px";
+                clearTimeout(currentTimeout);
+            }
+
+            // Handle any scheduled toasts after reload.
+            let potentialData = sessionStorage.getItem(STORAGE_KEY);
+            if (potentialData) {
+                sessionStorage.removeItem(STORAGE_KEY);
+                let data = JSON.parse(potentialData);
+                showDXToast(data.headerText, data.message, data.level, data.durationMs);
+            }
+
+            window.scheduleDXToast = scheduleDXToast;
+            window.showDXToast = showDXToast;
+            window.closeDXToast = closeDXToast;
+        </script>
+        <!-- CUSTOM HEAD -->
+    </head>
+    <body>
+        <div id="__dx-toast" class="dx-toast">
+            <div id="__dx-toast-inner" class="dx-toast-inner" style="right:-1000px;">
+                <div class="dx-toast-level-bar-container">
+                    <div id="__dx-toast-decor" class="dx-toast-level-bar __info"></div>
+                </div>
+                <div class="dx-toast-content">
+                    <div class="dx-toast-header">
+                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" preserveAspectRatio="none">
+                            <path d="M22.158 1.783c0 3.077-.851 5.482-2.215 7.377s-3.32 3.557-5.447 5.33-4.425 3.657-6.252 6.195-3.102 5.515-3.102 9.532h4.699c0-3.077.853-5.377 2.217-7.272s3.32-3.557 5.447-5.33 4.425-3.657 6.252-6.195 3.102-5.62 3.102-9.637z" fill="#e96020"/>
+                            <path d="M9.531 25.927c-.635 0-1.021.515-1.02 1.15s.385 1.151 1.02 1.15H22.47a1.151 1.151 0 1 0 0-2.301zm1.361-4.076c-.608 0-.954.558-.953 1.166s.346 1.035.953 1.035h10.217a1.101 1.101 0 1 0 0-2.201zm0-13.594a1.101 1.101 0 1 0 0 2.201h10.217c.607 0 .953-.598.953-1.205s-.345-.996-.953-.996zM9.531 4.021A1.15 1.15 0 0 0 8.38 5.17a1.15 1.15 0 0 0 1.15 1.15h12.94c.635 0 1.021-.498 1.02-1.133s-.386-1.166-1.02-1.166z" fill="#2d323b"/>
+                            <path d="M5.142 1.783c0 4.016 1.275 7.099 3.102 9.637s4.125 4.422 6.252 6.195 4.083 3.656 5.447 5.551 2.215 3.974 2.215 7.051h4.701c0-4.016-1.275-7.038-3.102-9.576s-4.125-4.422-6.252-6.195-4.083-3.435-5.447-5.33S9.841 4.86 9.841 1.783z" fill="#00a8d6"/>
+                        </svg>
+                        <h3 id="__dx-toast-text" class="dx-toast-header-text">Your app is being rebuilt.</h3>
+                    </div>
+                    <p id="__dx-toast-msg" class="dx-toast-msg">A non-hot-reloadable change occurred and we must rebuild.</p>
+                </div>
+            </div>
+        </div>
+        <div id="main"></div>
+        <!-- MODULE LOADER -->
+    </body>
+</html>

+ 0 - 0
packages/desktop/src/index.html → packages/desktop/src/assets/prod.index.html


+ 104 - 33
packages/desktop/src/launch.rs

@@ -8,6 +8,52 @@ use dioxus_document::eval;
 use std::any::Any;
 use tao::event::{Event, StartCause, WindowEvent};
 
+/// Launches the WebView and runs the event loop
+pub fn launch(root: fn() -> Element) {
+    launch_cfg(root, vec![], vec![]);
+}
+
+/// Launches the WebView and runs the event loop, with configuration and root props.
+pub fn launch_cfg(
+    root: fn() -> Element,
+    contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
+    platform_config: Vec<Box<dyn Any>>,
+) {
+    let mut virtual_dom = VirtualDom::new(root);
+
+    for context in contexts {
+        virtual_dom.insert_any_root_context(context());
+    }
+
+    let platform_config = *platform_config
+        .into_iter()
+        .find_map(|cfg| cfg.downcast::<Config>().ok())
+        .unwrap_or_default();
+
+    launch_virtual_dom(virtual_dom, platform_config)
+}
+
+/// Launches the WebView and runs the event loop, with configuration and root props.
+pub fn launch_virtual_dom(virtual_dom: VirtualDom, desktop_config: Config) -> ! {
+    #[cfg(feature = "tokio_runtime")]
+    {
+        tokio::runtime::Builder::new_multi_thread()
+            .enable_all()
+            .build()
+            .unwrap()
+            .block_on(tokio::task::unconstrained(async move {
+                launch_virtual_dom_blocking(virtual_dom, desktop_config)
+            }));
+
+        unreachable!("The desktop launch function will never exit")
+    }
+
+    #[cfg(not(feature = "tokio_runtime"))]
+    {
+        launch_virtual_dom_blocking(virtual_dom, desktop_config);
+    }
+}
+
 /// Launch the WebView and run the event loop, with configuration and root props.
 ///
 /// This will block the main thread, and *must* be spawned on the main thread. This function does not assume any runtime
@@ -111,43 +157,68 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, mut desktop_config:
     })
 }
 
-/// Launches the WebView and runs the event loop, with configuration and root props.
-pub fn launch_virtual_dom(virtual_dom: VirtualDom, desktop_config: Config) -> ! {
-    #[cfg(feature = "tokio_runtime")]
-    {
-        tokio::runtime::Builder::new_multi_thread()
-            .enable_all()
-            .build()
-            .unwrap()
-            .block_on(tokio::task::unconstrained(async move {
-                launch_virtual_dom_blocking(virtual_dom, desktop_config)
-            }));
+/// Expose the `Java_dev_dioxus_main_WryActivity_create` function to the JNI layer.
+/// We hardcode these to have a single trampoline for host Java code to call into.
+///
+/// This saves us from having to plumb the top-level package name all the way down into
+/// this file. This is better for modularity (ie just call dioxus' main to run the app) as
+/// well as cache thrashing since this crate doesn't rely on external env vars.
+///
+/// The CLI is expecting to find `dev.dioxus.main` in the final library. If you find a need to
+/// change this, you'll need to change the CLI as well.
+#[cfg(target_os = "android")]
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn start_app() {
+    wry::android_binding!(dev_dioxus, main, wry);
+    tao::android_binding!(
+        dev_dioxus,
+        main,
+        WryActivity,
+        wry::android_setup,
+        dioxus_main_root_fn,
+        tao
+    );
+}
 
-        unreachable!("The desktop launch function will never exit")
-    }
+#[cfg(target_os = "android")]
+#[doc(hidden)]
+pub fn dioxus_main_root_fn() {
+    // we're going to find the `main` symbol using dlsym directly and call it
+    unsafe {
+        let mut main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"main\0".as_ptr() as _);
 
-    #[cfg(not(feature = "tokio_runtime"))]
-    {
-        launch_virtual_dom_blocking(virtual_dom, desktop_config);
-    }
-}
+        if main_fn_ptr.is_null() {
+            main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"_main\0".as_ptr() as _);
+        }
 
-/// Launches the WebView and runs the event loop, with configuration and root props.
-pub fn launch(
-    root: fn() -> Element,
-    contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
-    platform_config: Vec<Box<dyn Any>>,
-) -> ! {
-    let mut virtual_dom = VirtualDom::new(root);
+        if main_fn_ptr.is_null() {
+            panic!("Failed to find main symbol");
+        }
 
-    for context in contexts {
-        virtual_dom.insert_any_root_context(context());
-    }
+        // Set the env vars that rust code might expect, passed off to us by the android app
+        // Doing this before main emulates the behavior of a regular executable
+        if cfg!(target_os = "android") && cfg!(debug_assertions) {
+            load_env_file_from_session_cache();
+        }
 
-    let platform_config = *platform_config
-        .into_iter()
-        .find_map(|cfg| cfg.downcast::<Config>().ok())
-        .unwrap_or_default();
+        let main_fn: extern "C" fn() = std::mem::transmute(main_fn_ptr);
+        main_fn();
+    };
+}
 
-    launch_virtual_dom(virtual_dom, platform_config)
+/// Load the env file from the session cache if we're in debug mode and on android
+///
+/// This is a slightly hacky way of being able to use std::env::var code in android apps without
+/// going through their custom java-based system.
+#[cfg(target_os = "android")]
+fn load_env_file_from_session_cache() {
+    let env_file = dioxus_cli_config::android_session_cache_dir().join(".env");
+    if let Some(env_file) = std::fs::read_to_string(&env_file).ok() {
+        for line in env_file.lines() {
+            if let Some((key, value)) = line.trim().split_once('=') {
+                std::env::set_var(key, value);
+            }
+        }
+    }
 }

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

@@ -19,7 +19,11 @@ const EVENTS_PATH: &str = "http://dioxus.index.html/__events";
 #[cfg(not(any(target_os = "android", target_os = "windows")))]
 const EVENTS_PATH: &str = "dioxus://index.html/__events";
 
-static DEFAULT_INDEX: &str = include_str!("./index.html");
+#[cfg(debug_assertions)]
+static DEFAULT_INDEX: &str = include_str!("./assets/dev.index.html");
+
+#[cfg(not(debug_assertions))]
+static DEFAULT_INDEX: &str = include_str!("./assets/prod.index.html");
 
 #[allow(clippy::too_many_arguments)] // just for now, should fix this eventually
 /// Handle a request from the webview

+ 26 - 1
packages/desktop/src/webview.rs

@@ -19,8 +19,8 @@ use dioxus_history::{History, MemoryHistory};
 use dioxus_hooks::to_owned;
 use dioxus_html::{HasFileData, HtmlEvent, PlatformEventData};
 use futures_util::{pin_mut, FutureExt};
-use std::cell::OnceCell;
 use std::sync::Arc;
+use std::{cell::OnceCell, time::Duration};
 use std::{rc::Rc, task::Waker};
 use wry::{DragDropEvent, RequestAsyncResponder, WebContext, WebViewBuilder};
 
@@ -480,6 +480,31 @@ impl WebviewInstance {
             .webview
             .evaluate_script("window.interpreter.kickAllStylesheetsOnPage()");
     }
+
+    /// Displays a toast to the developer.
+    pub(crate) fn show_toast(
+        &self,
+        header_text: &str,
+        message: &str,
+        level: &str,
+        duration: Duration,
+        after_reload: bool,
+    ) {
+        let as_ms = duration.as_millis();
+
+        let js_fn_name = match after_reload {
+            true => "scheduleDXToast",
+            false => "showDXToast",
+        };
+
+        _ = self.desktop_context.webview.evaluate_script(&format!(
+            r#"
+                if (typeof {js_fn_name} !== "undefined") {{
+                    window.{js_fn_name}("{header_text}", "{message}", "{level}", {as_ms});
+                }}
+                "#,
+        ));
+    }
 }
 
 /// A synchronous response to a browser event which may prevent the default browser's action

+ 27 - 1
packages/devtools/src/lib.rs

@@ -50,7 +50,33 @@ pub fn try_apply_changes(dom: &VirtualDom, msg: &HotReloadMsg) -> Result<(), Pat
 ///
 /// This doesn't use any form of security or protocol, so it's not safe to expose to the internet.
 #[cfg(not(target_arch = "wasm32"))]
-pub fn connect(endpoint: String, mut callback: impl FnMut(DevserverMsg) + Send + 'static) {
+pub fn connect(callback: impl FnMut(DevserverMsg) + Send + 'static) {
+    let Some(endpoint) = dioxus_cli_config::devserver_ws_endpoint() else {
+        return;
+    };
+
+    connect_at(endpoint, callback);
+}
+
+/// Connect to the devserver and handle hot-patch messages only, implementing the subsecond hotpatch
+/// protocol.
+///
+/// This is intended to be used by non-dioxus projects that want to use hotpatching.
+///
+/// To handle the full devserver protocol, use `connect` instead.
+#[cfg(not(target_arch = "wasm32"))]
+pub fn connect_subsecond() {
+    connect(|msg| {
+        if let DevserverMsg::HotReload(hot_reload_msg) = msg {
+            if let Some(jumptable) = hot_reload_msg.jump_table {
+                unsafe { subsecond::apply_patch(jumptable).unwrap() };
+            }
+        }
+    });
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub fn connect_at(endpoint: String, mut callback: impl FnMut(DevserverMsg) + Send + 'static) {
     std::thread::spawn(move || {
         let uri = format!(
             "{endpoint}?aslr_reference={}&build_id={}",

+ 1 - 1
packages/dioxus/Cargo.toml

@@ -80,7 +80,7 @@ router = ["dep:dioxus-router"]
 # Platforms
 fullstack = ["dep:dioxus-fullstack", "dioxus-config-macro/fullstack", "dep:serde"]
 desktop = ["dep:dioxus-desktop", "dioxus-fullstack?/desktop", "dioxus-config-macro/desktop"]
-mobile = ["dep:dioxus-mobile", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobile"]
+mobile = ["dep:dioxus-mobile", "dep:dioxus-desktop", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobile"]
 web = [
   "dep:dioxus-web",
   "dioxus-fullstack?/web",

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

@@ -332,12 +332,12 @@ impl LaunchBuilder {
 
         #[cfg(feature = "mobile")]
         if matches!(platform, KnownPlatform::Mobile) {
-            return dioxus_mobile::launch_bindings::launch(app, contexts, configs);
+            return dioxus_desktop::launch::launch_cfg(app, contexts, configs);
         }
 
         #[cfg(feature = "desktop")]
         if matches!(platform, KnownPlatform::Desktop) {
-            return dioxus_desktop::launch::launch(app, contexts, configs);
+            return dioxus_desktop::launch::launch_cfg(app, contexts, configs);
         }
 
         #[cfg(feature = "server")]

+ 1 - 3
packages/liveview/src/pool.rs

@@ -119,9 +119,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
     #[cfg(all(feature = "devtools", debug_assertions))]
     let mut hot_reload_rx = {
         let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
-        if let Some(endpoint) = dioxus_cli_config::devserver_ws_endpoint() {
-            dioxus_devtools::connect(endpoint, move |template| _ = tx.send(template));
-        }
+        dioxus_devtools::connect(move |template| _ = tx.send(template));
         rx
     };
 

+ 1 - 139
packages/mobile/src/lib.rs

@@ -5,23 +5,6 @@
 pub use dioxus_desktop::*;
 use dioxus_lib::prelude::*;
 use std::any::Any;
-use std::sync::Mutex;
-
-pub mod launch_bindings {
-
-    use super::*;
-    pub fn launch(
-        root: fn() -> Element,
-        contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
-        platform_config: Vec<Box<dyn Any>>,
-    ) {
-        super::launch_cfg(root, contexts, platform_config);
-    }
-
-    pub fn launch_virtual_dom(_virtual_dom: VirtualDom, _desktop_config: Config) -> ! {
-        todo!()
-    }
-}
 
 /// Launch via the binding API
 pub fn launch(root: fn() -> Element) {
@@ -33,126 +16,5 @@ pub fn launch_cfg(
     contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
     platform_config: Vec<Box<dyn Any>>,
 ) {
-    #[cfg(target_os = "android")]
-    {
-        *APP_OBJECTS.lock().unwrap() = Some(BoundLaunchObjects {
-            root,
-            contexts,
-            platform_config,
-        });
-    }
-
-    #[cfg(not(target_os = "android"))]
-    {
-        dioxus_desktop::launch::launch(root, contexts, platform_config);
-    }
-}
-
-/// 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_OBJECTS
-        .lock()
-        .expect("APP_FN_PTR lock failed")
-        .take()
-        .expect("Android to have set the app trampoline");
-
-    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.
-/// We hardcode these to have a single trampoline for host Java code to call into.
-///
-/// This saves us from having to plumb the top-level package name all the way down into
-/// this file. This is better for modularity (ie just call dioxus' main to run the app) as
-/// well as cache thrashing since this crate doesn't rely on external env vars.
-///
-/// The CLI is expecting to find `dev.dioxus.main` in the final library. If you find a need to
-/// change this, you'll need to change the CLI as well.
-#[cfg(target_os = "android")]
-#[no_mangle]
-#[inline(never)]
-pub extern "C" fn start_app() {
-    tao::android_binding!(dev_dioxus, main, WryActivity, wry::android_setup, root, tao);
-    wry::android_binding!(dev_dioxus, main, wry);
-}
-
-/// Call our `main` function to initialize the rust runtime and set the launch binding trampoline
-#[cfg(target_os = "android")]
-#[no_mangle]
-#[inline(never)]
-pub extern "C" fn JNI_OnLoad(
-    _vm: *mut libc::c_void,
-    _reserved: *mut libc::c_void,
-) -> jni::sys::jint {
-    // we're going to find the `main` symbol using dlsym directly and call it
-    unsafe {
-        let mut main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"main\0".as_ptr() as _);
-
-        if main_fn_ptr.is_null() {
-            main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"_main\0".as_ptr() as _);
-        }
-
-        if main_fn_ptr.is_null() {
-            panic!("Failed to find main symbol");
-        }
-
-        // Set the env vars that rust code might expect, passed off to us by the android app
-        // Doing this before main emulates the behavior of a regular executable
-        if cfg!(target_os = "android") && cfg!(debug_assertions) {
-            load_env_file_from_session_cache();
-        }
-
-        let main_fn: extern "C" fn() = std::mem::transmute(main_fn_ptr);
-        main_fn();
-    };
-
-    jni::sys::JNI_VERSION_1_6
-}
-
-/// Load the env file from the session cache if we're in debug mode and on android
-///
-/// This is a slightly hacky way of being able to use std::env::var code in android apps without
-/// going through their custom java-based system.
-#[cfg(target_os = "android")]
-fn load_env_file_from_session_cache() {
-    let env_file = dioxus_cli_config::android_session_cache_dir().join(".env");
-    if let Some(env_file) = std::fs::read_to_string(&env_file).ok() {
-        for line in env_file.lines() {
-            if let Some((key, value)) = line.trim().split_once('=') {
-                std::env::set_var(key, value);
-            }
-        }
-    }
+    dioxus_desktop::launch::launch_cfg(root, contexts, platform_config);
 }

+ 5 - 7
packages/native/src/lib.rs

@@ -69,13 +69,11 @@ pub fn launch_cfg_with_props<P: Clone + 'static, M: 'static>(
     ))]
     {
         use crate::event::DioxusNativeEvent;
-        if let Some(endpoint) = dioxus_cli_config::devserver_ws_endpoint() {
-            let proxy = event_loop.create_proxy();
-            dioxus_devtools::connect(endpoint, move |event| {
-                let dxn_event = DioxusNativeEvent::DevserverEvent(event);
-                let _ = proxy.send_event(BlitzShellEvent::embedder_event(dxn_event));
-            })
-        }
+        let proxy = event_loop.create_proxy();
+        dioxus_devtools::connect(move |event| {
+            let dxn_event = DioxusNativeEvent::DevserverEvent(event);
+            let _ = proxy.send_event(BlitzShellEvent::embedder_event(dxn_event));
+        })
     }
 
     // Spin up the virtualdom

+ 1 - 5
packages/server/src/launch.rs

@@ -51,11 +51,7 @@ async fn serve_server(
 ) {
     let (devtools_tx, mut devtools_rx) = futures_channel::mpsc::unbounded();
 
-    if let Some(endpoint) = dioxus_cli_config::devserver_ws_endpoint() {
-        dioxus_devtools::connect(endpoint, move |msg| {
-            _ = devtools_tx.unbounded_send(msg);
-        })
-    }
+    dioxus_devtools::connect(move |msg| _ = devtools_tx.unbounded_send(msg));
 
     let platform_config = platform_config
         .into_iter()

+ 1 - 3
packages/subsecond/subsecond/src/lib.rs

@@ -756,8 +756,6 @@ macro_rules! impl_hot_function {
                 unsafe fn call_as_ptr(&mut self, args: ($($arg,)*)) -> Self::Return {
                     unsafe {
                         if let Some(jump_table) = get_jump_table() {
-
-
                             let real = std::mem::transmute_copy::<Self, Self::Real>(&self) as *const ();
 
                             // Android implements MTE / pointer tagging and we need to preserve the tag.
@@ -765,7 +763,7 @@ macro_rules! impl_hot_function {
                             // This is only implemented on 64-bit platforms since pointer tagging is not available on 32-bit platforms
                             // In dev, Dioxus disables MTE to work around this issue, but we still handle it anyways.
                             #[cfg(all(target_pointer_width = "64", target_os = "android"))] let nibble  = real as u64 & 0xFF00_0000_0000_0000;
-                            #[cfg(target_pointer_width = "64")] let real    = real as u64 & 0x00FFF_FFF_FFFF_FFFF;
+                            #[cfg(all(target_pointer_width = "64", target_os = "android"))] let real    = real as u64 & 0x00FFF_FFF_FFFF_FFFF;
 
                             #[cfg(target_pointer_width = "64")] let real  = real as u64;
 

+ 8 - 1
packages/wasm-split/wasm-split-cli/Cargo.toml

@@ -1,7 +1,14 @@
 [package]
 name = "wasm-split-cli"
-version = "0.1.0"
 edition = "2021"
+version = { workspace = true }
+authors = ["Jonathan Kelley"]
+description = "CLI-support for wasm-split - a tool for splitting up large WASM binaries into smaller chunks"
+repository = "https://github.com/DioxusLabs/dioxus/"
+license = "MIT OR Apache-2.0"
+keywords = ["wasm", "cli", "split", "dioxus"]
+rust-version = "1.81.0"
+
 
 [dependencies]
 anyhow =  { workspace = true }

+ 8 - 1
packages/wasm-split/wasm-split-macro/Cargo.toml

@@ -1,7 +1,14 @@
 [package]
 name = "wasm-split-macro"
-version = "0.1.0"
 edition = "2021"
+version = { workspace = true }
+authors = ["Jonathan Kelley"]
+description = "macro crate for wasm-split - a tool for splitting up large WASM binaries into smaller chunks"
+repository = "https://github.com/DioxusLabs/dioxus/"
+license = "MIT OR Apache-2.0"
+keywords = ["wasm", "cli", "split", "dioxus"]
+rust-version = "1.81.0"
+
 
 [dependencies]
 syn = { workspace = true, features = ["full"] }

+ 8 - 1
packages/wasm-split/wasm-split/Cargo.toml

@@ -1,7 +1,14 @@
 [package]
 name = "wasm-split"
-version = "0.1.0"
+version = { workspace = true }
 edition = "2021"
+authors = ["Jonathan Kelley"]
+description = "A tool for splitting up large WASM binaries into smaller chunks"
+repository = "https://github.com/DioxusLabs/dioxus/"
+license = "MIT OR Apache-2.0"
+keywords = ["wasm", "cli", "split", "dioxus"]
+rust-version = "1.81.0"
+
 
 [dependencies]
 async-once-cell = { workspace = true, features = ["std"] }