diff --git a/.github/install.sh b/.github/install.sh index a16c7b126..56dfe22e2 100755 --- a/.github/install.sh +++ b/.github/install.sh @@ -47,19 +47,31 @@ success() { 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")' +if [[ $# -gt 2 ]]; then + error 'Too many arguments, only 2 are allowed. The first can be a specific tag of dx to install. (e.g. "dx-v0.7.1") or `nightly` or `pr ` to install the latest nightly or PR build.' fi if [ "$OS" = "Windows_NT" ]; then - target="x86_64-pc-windows-msvc" + 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 + case $(uname -sm) in + "Darwin x86_64") target="x86_64-apple-darwin" ;; + "Darwin arm64") target="aarch64-apple-darwin" ;; + "Linux aarch64") + if [ -f /etc/alpine-release ]; then + target="aarch64-unknown-linux-musl" + else + target="aarch64-unknown-linux-gnu" + fi + ;; + *) + if [ -f /etc/alpine-release ]; then + target="x86_64-unknown-linux-musl" + else + target="x86_64-unknown-linux-gnu" + fi + ;; + esac fi GITHUB=${GITHUB-"https://github.com"} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 845ff27b8..0b31ea87d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -82,7 +82,7 @@ jobs: with: cache-all-crates: "true" - uses: browser-actions/setup-firefox@latest - - run: cargo test --lib --bins --tests --examples --workspace --exclude dioxus-desktop --exclude dioxus-mobile + - run: cargo test --lib --bins --tests --examples --workspace --exclude dioxus-desktop release-test: if: github.event.pull_request.draft == false @@ -103,7 +103,7 @@ jobs: with: cache-all-crates: "true" - uses: browser-actions/setup-firefox@latest - - run: cargo test --lib --bins --tests --examples --workspace --exclude dioxus-desktop --exclude dioxus-mobile --profile release-unoptimized + - run: cargo test --lib --bins --tests --examples --workspace --exclude dioxus-desktop --profile release-unoptimized fmt: if: github.event.pull_request.draft == false @@ -210,7 +210,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 16 + node-version: 24 - name: Install Rust uses: dtolnay/rust-toolchain@master with: @@ -256,6 +256,7 @@ jobs: cross: false, command: "test", args: "--all --tests", + platform: "desktop", } - { target: x86_64-apple-darwin, @@ -264,6 +265,7 @@ jobs: cross: false, command: "test", args: "--all --tests", + platform: "desktop", } - { target: aarch64-apple-ios, @@ -271,7 +273,8 @@ jobs: toolchain: "1.86.0", cross: false, command: "build", - args: "--package dioxus-mobile", + args: "--package dioxus-desktop", + platform: "ios", } - { target: x86_64-unknown-linux-gnu, @@ -280,6 +283,7 @@ jobs: cross: false, command: "build", args: "--all --tests", + platform: "desktop", } - { target: aarch64-unknown-linux-gnu, @@ -288,6 +292,7 @@ jobs: cross: false, command: "build", args: "--all --tests", + platform: "desktop", } - { target: aarch64-linux-android, @@ -295,7 +300,8 @@ jobs: toolchain: "1.86.0", cross: true, command: "build", - args: "--package dioxus-mobile", + args: "--package dioxus-desktop", + platform: "android", } steps: @@ -386,3 +392,99 @@ jobs: # with: # manifest-path: ./Cargo.toml # exclude: "dioxus-cli, dioxus-ext" + + # Releases for the CLI on a per-branch basis + # nightly-cli: + # if: github.event.pull_request.draft == false + # runs-on: ${{ matrix.platform.os }} + # strategy: + # matrix: + # platform: + # - { + # target: x86_64-pc-windows-msvc, + # os: windows-latest + # } + # - { + # target: aarch64-pc-windows-msvc, + # os: windows-latest + # } + # - { + # target: x86_64-apple-darwin, + # os: macos-13 + # } + # - { + # target: aarch64-apple-darwin, + # os: macos-latest + # } + # - { + # target: x86_64-unknown-linux-gnu, + # os: ubuntu-24.04 + # } + # - { + # target: aarch64-unknown-linux-gnu, + # os: ubuntu-24.04-arm + # } + # - { + # target: x86_64-unknown-linux-musl, + # os: ubuntu-24.04 + # } + # - { + # target: aarch64-unknown-linux-musl, + # os: ubuntu-24.04-arm + # } + # steps: + # - name: Checkout + # uses: actions/checkout@v4 + + # - name: Install openssl on macos + # if: matrix.platform.os == 'macos-latest' + # run: brew install openssl + + # - name: Install nasm for windows (tls) + # if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }} + # uses: ilammy/setup-nasm@v1 + + # - name: Free Disk Space + # if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }} + # uses: ./.github/actions/free-disk-space + + # - uses: awalsh128/cache-apt-pkgs-action@latest + # if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }} + # with: + # packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev musl-tools + # version: 1.0 + + # - name: Install stable + # uses: dtolnay/rust-toolchain@master + # with: + # toolchain: "1.86.0" + # targets: ${{ matrix.platform.target }} + + # - uses: Swatinem/rust-cache@v2 + # with: + # cache-all-crates: "true" + # save-if: ${{ github.ref == 'refs/heads/main' }} + + # - name: Free Disk Space + # uses: jlumbroso/free-disk-space@v1.3.1 + # with: # speed things up a bit + # large-packages: false + # docker-images: false + # swap-storage: false + + # # Todo: we want `cargo install dx` to actually just use a prebuilt binary instead of building it + # - name: Build CLI + # run: | + # cargo build --release --package dioxus-cli --target ${{ matrix.platform.target }} + + # - name: Upload CLI binaries + # uses: actions/upload-artifact@v4 + # if: always() + # with: + # name: dx-${{ matrix.platform.target }}-${{ github.event.number || github.ref_name }} + # path: ./target/${{ matrix.platform.target }}/release/dx + # retention-days: ${{ github.ref == 'refs/heads/main' && 90 || 30 }} + # overwrite: true + + # - name: Output artifact ID + # run: echo 'Artifact ID is ${{ steps.artifact-upload-step.outputs.artifact-id }}' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f5197aaa0..459ebd2ad 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -61,6 +61,10 @@ jobs: os: ubuntu-24.04 - target: aarch64-unknown-linux-gnu os: ubuntu-24.04-arm + - target: x86_64-unknown-linux-musl + os: ubuntu-24.04 + - target: aarch64-unknown-linux-musl + os: ubuntu-24.04-arm steps: - name: Checkout uses: actions/checkout@v4 @@ -80,7 +84,7 @@ jobs: - uses: awalsh128/cache-apt-pkgs-action@latest if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }} with: - packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev + packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev musl-tools version: 1.0 - name: Install stable diff --git a/Cargo.lock b/Cargo.lock index 246bd241b..3f6d6b464 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -340,7 +340,7 @@ dependencies = [ "bitflags 2.9.1", "cc", "cesu8", - "jni", + "jni 0.21.1", "jni-sys", "libc", "log", @@ -623,7 +623,7 @@ dependencies = [ "flate2", "scroll 0.12.0", "serde", - "serde-xml-rs", + "serde-xml-rs 0.6.0", "thiserror 1.0.69", ] @@ -646,7 +646,7 @@ dependencies = [ "reqwest 0.11.27", "scroll 0.12.0", "serde", - "serde-xml-rs", + "serde-xml-rs 0.6.0", "sha1", "sha2", "signature", @@ -2960,6 +2960,46 @@ dependencies = [ "cipher", ] +[[package]] +name = "bluetooth-scanner" +version = "0.1.1" +dependencies = [ + "btleplug", + "dioxus", + "futures", + "futures-channel", + "tokio", +] + +[[package]] +name = "bluez-async" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ce74d1f22603b879c3a99bd30eddc423460fb73a802655ca615b623ff3a838" +dependencies = [ + "bitflags 2.9.1", + "bluez-generated", + "dbus", + "dbus-tokio", + "futures", + "itertools 0.14.0", + "log", + "serde", + "serde-xml-rs 0.8.1", + "thiserror 2.0.12", + "tokio", + "uuid", +] + +[[package]] +name = "bluez-generated" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676783265eadd6f11829982792c6f303f3854d014edfba384685dcf237dd062" +dependencies = [ + "dbus", +] + [[package]] name = "borsh" version = "1.5.7" @@ -3071,6 +3111,34 @@ dependencies = [ "serde", ] +[[package]] +name = "btleplug" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a11621cb2c8c024e444734292482b1ad86fb50ded066cf46252e46643c8748" +dependencies = [ + "async-trait", + "bitflags 2.9.1", + "bluez-async", + "dashmap 6.1.0", + "dbus", + "futures", + "jni 0.19.0", + "jni-utils", + "log", + "objc2 0.5.2", + "objc2-core-bluetooth", + "objc2-foundation 0.2.2", + "once_cell", + "static_assertions", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "uuid", + "windows 0.61.3", + "windows-future", +] + [[package]] name = "buffer-redux" version = "1.0.2" @@ -4134,7 +4202,7 @@ dependencies = [ "core-foundation-sys", "coreaudio-rs", "dasp_sample", - "jni", + "jni 0.21.1", "js-sys", "libc", "mach2", @@ -4723,6 +4791,30 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "libdbus-sys", + "winapi", +] + +[[package]] +name = "dbus-tokio" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007688d459bc677131c063a3a77fb899526e17b7980f390b69644bdbc41fad13" +dependencies = [ + "dbus", + "libc", + "tokio", +] + [[package]] name = "debugid" version = "0.8.0" @@ -4924,7 +5016,6 @@ dependencies = [ "dioxus-html", "dioxus-liveview", "dioxus-logger", - "dioxus-mobile", "dioxus-native", "dioxus-router", "dioxus-server", @@ -4952,7 +5043,7 @@ dependencies = [ "dioxus-cli-config", "http 1.3.1", "infer", - "jni", + "jni 0.21.1", "ndk 0.9.0", "ndk-context", "ndk-sys 0.6.0+11769913", @@ -5257,7 +5348,7 @@ dependencies = [ "global-hotkey", "http-range", "infer", - "jni", + "jni 0.21.1", "lazy-js-bundle", "libc", "muda", @@ -5267,6 +5358,7 @@ dependencies = [ "objc", "objc_id", "open", + "openssl", "percent-encoding", "pollster", "rand 0.8.5", @@ -5399,7 +5491,6 @@ dependencies = [ "dioxus-interpreter-js", "dioxus-isrg", "dioxus-lib", - "dioxus-mobile", "dioxus-router", "dioxus-server", "dioxus-web", @@ -5607,17 +5698,6 @@ dependencies = [ "tracing-wasm", ] -[[package]] -name = "dioxus-mobile" -version = "0.7.0-alpha.2" -dependencies = [ - "dioxus-cli-config", - "dioxus-desktop", - "dioxus-lib", - "jni", - "libc", -] - [[package]] name = "dioxus-native" version = "0.7.0-alpha.2" @@ -6608,6 +6688,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "faster-hex" version = "0.9.0" @@ -7391,7 +7483,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" dependencies = [ - "fallible-iterator", + "fallible-iterator 0.2.0", "indexmap 1.9.3", "stable_deref_trait", ] @@ -8318,6 +8410,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "hashlink" version = "0.10.0" @@ -8458,6 +8559,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "hotdog" +version = "0.1.0" +dependencies = [ + "dioxus", + "reqwest 0.12.22", + "rusqlite", + "serde", + "serde_json", +] + [[package]] name = "hstr" version = "0.2.17" @@ -9322,15 +9434,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071ed4cc1afd86650602c7b11aa2e1ce30762a1c27193201cb5cee9c6ebb1294" -[[package]] -name = "itertools" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.10.5" @@ -9437,6 +9540,20 @@ dependencies = [ "jiff-tzdb", ] +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", +] + [[package]] name = "jni" version = "0.21.1" @@ -9459,6 +9576,21 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jni-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259e9f2c3ead61de911f147000660511f07ab00adeed1d84f5ac4d0386e7a6c4" +dependencies = [ + "dashmap 5.5.3", + "futures", + "jni 0.19.0", + "log", + "once_cell", + "static_assertions", + "uuid", +] + [[package]] name = "jobserver" version = "0.1.33" @@ -9765,6 +9897,15 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + [[package]] name = "libfuzzer-sys" version = "0.4.9" @@ -11108,6 +11249,17 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-bluetooth" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-core-data" version = "0.2.2" @@ -11386,7 +11538,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" dependencies = [ - "jni", + "jni 0.21.1", "ndk 0.8.0", "ndk-context", "num-derive", @@ -11865,7 +12017,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82040a392923abe6279c00ab4aff62d5250d1c8555dc780e4b02783a7aa74863" dependencies = [ - "fallible-iterator", + "fallible-iterator 0.2.0", "scroll 0.11.0", "uuid", ] @@ -13505,6 +13657,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.9.1", + "fallible-iterator 0.3.0", + "fallible-streaming-iterator", + "hashlink 0.9.1", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rust_decimal" version = "1.37.2" @@ -14136,6 +14302,18 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "serde-xml-rs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53630160a98edebde0123eb4dfd0fce6adff091b2305db3154a9e920206eb510" +dependencies = [ + "log", + "serde", + "thiserror 1.0.69", + "xml-rs", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -14790,7 +14968,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.15.4", - "hashlink", + "hashlink 0.10.0", "indexmap 2.10.0", "ipnet", "ipnetwork", @@ -16045,7 +16223,7 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "lazy_static", "libc", "log", @@ -18508,27 +18686,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" -[[package]] -name = "wifi-scanner" -version = "0.1.1" -dependencies = [ - "dioxus", - "futures", - "futures-channel", - "tokio", - "wifiscanner", -] - -[[package]] -name = "wifiscanner" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d74e667d34abc74f1e5b509ae3d24641f56514ba2c5a64f9d733ae2061aef856" -dependencies = [ - "itertools 0.8.2", - "regex", -] - [[package]] name = "winapi" version = "0.3.9" @@ -19257,7 +19414,7 @@ dependencies = [ "html5ever", "http 1.3.1", "javascriptcore-rs", - "jni", + "jni 0.21.1", "kuchikiki", "libc", "ndk 0.9.0", diff --git a/Cargo.toml b/Cargo.toml index 7890d514d..a66abdffa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ members = [ "packages/web", "packages/ssr", "packages/desktop", - "packages/mobile", "packages/interpreter", "packages/liveview", "packages/autofmt", @@ -48,7 +47,6 @@ members = [ "packages/interpreter", "packages/lazy-js-bundle", "packages/liveview", - "packages/mobile", "packages/router-macro", "packages/router", "packages/rsx-hotreload", @@ -102,8 +100,9 @@ members = [ # Full project examples "example-projects/fullstack-hackernews", "example-projects/ecommerce-site", - "example-projects/wifi-scanner", + "example-projects/bluetooth-scanner", "example-projects/file-explorer", + "example-projects/hotdog", # Simple examples that require a crate "examples/tailwind", @@ -155,7 +154,6 @@ dioxus-web = { path = "packages/web", version = "0.7.0-alpha.2", default-feature dioxus-isrg = { path = "packages/isrg", version = "0.7.0-alpha.2" } dioxus-ssr = { path = "packages/ssr", version = "0.7.0-alpha.2", default-features = false } dioxus-desktop = { path = "packages/desktop", version = "0.7.0-alpha.2", default-features = false } -dioxus-mobile = { path = "packages/mobile", version = "0.7.0-alpha.2" } dioxus-interpreter-js = { path = "packages/interpreter", version = "0.7.0-alpha.2" } dioxus-liveview = { path = "packages/liveview", version = "0.7.0-alpha.2" } dioxus-autofmt = { path = "packages/autofmt", version = "0.7.0-alpha.2" } diff --git a/example-projects/wifi-scanner/.gitignore b/example-projects/bluetooth-scanner/.gitignore similarity index 100% rename from example-projects/wifi-scanner/.gitignore rename to example-projects/bluetooth-scanner/.gitignore diff --git a/example-projects/wifi-scanner/Cargo.toml b/example-projects/bluetooth-scanner/Cargo.toml similarity index 88% rename from example-projects/wifi-scanner/Cargo.toml rename to example-projects/bluetooth-scanner/Cargo.toml index 91b43d91c..c9ed65c9c 100644 --- a/example-projects/wifi-scanner/Cargo.toml +++ b/example-projects/bluetooth-scanner/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "wifi-scanner" +name = "bluetooth-scanner" version = "0.1.1" edition = "2018" publish = false @@ -11,4 +11,4 @@ tokio = { workspace = true, features = ["full"] } dioxus = { workspace = true, features = ["desktop"] } futures-channel = { workspace = true } futures = { workspace = true } -wifiscanner = "0.5.1" +btleplug = "0.11.8" diff --git a/example-projects/wifi-scanner/README.md b/example-projects/bluetooth-scanner/README.md similarity index 79% rename from example-projects/wifi-scanner/README.md rename to example-projects/bluetooth-scanner/README.md index 8107985bc..9c11d40ff 100644 --- a/example-projects/wifi-scanner/README.md +++ b/example-projects/bluetooth-scanner/README.md @@ -1,4 +1,4 @@ -# WiFi scanner app +# Bluetooth scanner app This desktop app showcases the use of background threads. diff --git a/example-projects/bluetooth-scanner/assets/tailwind.css b/example-projects/bluetooth-scanner/assets/tailwind.css new file mode 100644 index 000000000..518d2b800 --- /dev/null +++ b/example-projects/bluetooth-scanner/assets/tailwind.css @@ -0,0 +1,418 @@ +/*! tailwindcss v4.1.5 | MIT License | https://tailwindcss.com */ +@layer properties; +@layer theme, base, components, utilities; +@layer theme { + :root, :host { + --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', + monospace; + --color-green-500: oklch(72.3% 0.219 149.579); + --color-indigo-500: oklch(58.5% 0.233 277.117); + --color-indigo-600: oklch(51.1% 0.262 276.966); + --color-purple-50: oklch(97.7% 0.014 308.299); + --color-purple-500: oklch(62.7% 0.265 303.9); + --color-gray-50: oklch(98.5% 0.002 247.839); + --color-gray-500: oklch(55.1% 0.027 264.364); + --color-white: #fff; + --spacing: 0.25rem; + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.75); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); + --font-weight-medium: 500; + --font-weight-bold: 700; + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + } +} +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden='until-found'])) { + display: none !important; + } +} +@layer utilities { + .container { + width: 100%; + @media (width >= 40rem) { + max-width: 40rem; + } + @media (width >= 48rem) { + max-width: 48rem; + } + @media (width >= 64rem) { + max-width: 64rem; + } + @media (width >= 80rem) { + max-width: 80rem; + } + @media (width >= 96rem) { + max-width: 96rem; + } + } + .mx-auto { + margin-inline: auto; + } + .mb-6 { + margin-bottom: calc(var(--spacing) * 6); + } + .flex { + display: flex; + } + .inline-block { + display: inline-block; + } + .table { + display: table; + } + .w-full { + width: 100%; + } + .table-auto { + table-layout: auto; + } + .overflow-x-auto { + overflow-x: auto; + } + .rounded { + border-radius: 0.25rem; + } + .rounded-full { + border-radius: calc(infinity * 1px); + } + .bg-gray-50 { + background-color: var(--color-gray-50); + } + .bg-green-500 { + background-color: var(--color-green-500); + } + .bg-indigo-500 { + background-color: var(--color-indigo-500); + } + .bg-purple-50 { + background-color: var(--color-purple-50); + } + .bg-white { + background-color: var(--color-white); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .px-6 { + padding-inline: calc(var(--spacing) * 6); + } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } + .py-3 { + padding-block: calc(var(--spacing) * 3); + } + .py-5 { + padding-block: calc(var(--spacing) * 5); + } + .py-8 { + padding-block: calc(var(--spacing) * 8); + } + .pb-3 { + padding-bottom: calc(var(--spacing) * 3); + } + .pl-6 { + padding-left: calc(var(--spacing) * 6); + } + .text-left { + text-align: left; + } + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } + .text-xs { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + .font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + .text-gray-500 { + color: var(--color-gray-500); + } + .text-purple-500 { + color: var(--color-purple-500); + } + .text-white { + color: var(--color-white); + } + .shadow { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .transition { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .duration-200 { + --tw-duration: 200ms; + transition-duration: 200ms; + } + .hover\:bg-indigo-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-indigo-600); + } + } + } + .md\:w-auto { + @media (width >= 48rem) { + width: auto; + } + } +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-duration { + syntax: "*"; + inherits: false; +} +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-font-weight: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --tw-duration: initial; + } + } +} diff --git a/example-projects/wifi-scanner/demo_small.png b/example-projects/bluetooth-scanner/demo_small.png similarity index 100% rename from example-projects/wifi-scanner/demo_small.png rename to example-projects/bluetooth-scanner/demo_small.png diff --git a/example-projects/wifi-scanner/src/main.rs b/example-projects/bluetooth-scanner/src/main.rs similarity index 61% rename from example-projects/wifi-scanner/src/main.rs rename to example-projects/bluetooth-scanner/src/main.rs index 93d7061ad..c53498445 100644 --- a/example-projects/wifi-scanner/src/main.rs +++ b/example-projects/bluetooth-scanner/src/main.rs @@ -1,38 +1,45 @@ use dioxus::prelude::*; -use wifiscanner::Wifi; fn main() { dioxus::launch(app) } -fn perform_scan() -> Status { - if let Ok(devices) = wifiscanner::scan() { - if devices.is_empty() { - Status::NoneFound - } else { - Status::Found(devices) - } - } else { - Status::NoneFound - } -} - -enum Status { - NoneFound, - Found(Vec), -} - fn app() -> Element { - let mut status = - use_resource(|| async { tokio::task::spawn_blocking(perform_scan).await.unwrap() }); + let mut status = use_resource(|| async { + use btleplug::api::{Central, Manager as _, Peripheral, ScanFilter}; + + let manager = btleplug::platform::Manager::new().await.unwrap(); + + // get the first bluetooth adapter + let adapters = manager.adapters().await.unwrap(); + let central = adapters.into_iter().next().unwrap(); + + // start scanning for devices + central.start_scan(ScanFilter::default()).await.unwrap(); + + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + + // Return the list of peripherals after scanning + let mut devices = vec![]; + for p in central.peripherals().await.unwrap() { + if let Some(p) = p.properties().await.unwrap() { + devices.push(p); + } + } + + println!("Found {} Bluetooth devices", devices.len()); + + devices + }); + let scanning = !status.finished(); rsx! { - link { rel: "stylesheet", href: "https://unpkg.com/tailwindcss@^2.0/dist/tailwind.min.css" }, + link { rel: "stylesheet", href: asset!("/assets/tailwind.css") }, div { div { class: "py-8 px-6", div { class: "container px-4 mx-auto", - h2 { class: "text-2xl font-bold", "Scan for WiFi Networks" } + h2 { class: "text-2xl font-bold", "Scan for Bluetooth Devices" } button { class: "inline-block w-full md:w-auto px-6 py-3 font-medium text-white bg-indigo-500 hover:bg-indigo-600 rounded transition duration-200", disabled: scanning, @@ -58,24 +65,20 @@ fn app() -> Element { } match &*status.read() { - None => rsx!(""), - Some(Status::NoneFound) => rsx!("No networks found. Try scanning again"), - Some(Status::Found(wifis)) => { + None => rsx!("no devices yet!"), + Some(peripherals) => { // Create vector of tuples of (signal_level, wifi) for sorting by signal_level - let mut sorted_wifis = wifis - .iter() - .map(|wif: &Wifi| (wif, wif.signal_level.parse::().unwrap())) - .collect::>(); - sorted_wifis.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); + let mut sorted_devices = peripherals.clone(); + sorted_devices.sort_by(|a, b| a.rssi.partial_cmp(&b.rssi).unwrap()); rsx! { tbody { - for (Wifi { mac: _, ssid, channel, signal_level, security }, _) in sorted_wifis.into_iter().rev() { + for peripheral in sorted_devices.into_iter().rev() { tr { class: "text-xs bg-gray-50", - td { class: "py-5 px-6 font-medium", "{signal_level}" } - td { class: "flex py-3 font-medium", "{ssid}" } - td { span { class: "inline-block py-1 px-2 text-white bg-green-500 rounded-full", "{channel}" } } - td { span { class: "inline-block py-1 px-2 text-purple-500 bg-purple-50 rounded-full", "{security}" } } + td { class: "py-5 px-6 font-medium", "{peripheral.rssi.unwrap_or(-100)}" } + td { class: "flex py-3 font-medium", "{peripheral.local_name.clone().unwrap_or_default()}" } + td { span { class: "inline-block py-1 px-2 text-white bg-green-500 rounded-full", "{peripheral.address}" } } + td { span { class: "inline-block py-1 px-2 text-purple-500 bg-purple-50 rounded-full", "{peripheral.tx_power_level.unwrap_or_default()}" } } } } } diff --git a/example-projects/bluetooth-scanner/tailwind.css b/example-projects/bluetooth-scanner/tailwind.css new file mode 100644 index 000000000..81496da7b --- /dev/null +++ b/example-projects/bluetooth-scanner/tailwind.css @@ -0,0 +1,2 @@ +@import "tailwindcss"; +@source "./src/**/*.{rs,html,css}"; diff --git a/example-projects/hotdog/Cargo.toml b/example-projects/hotdog/Cargo.toml new file mode 100644 index 000000000..e97df95cc --- /dev/null +++ b/example-projects/hotdog/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "hotdog" +version = "0.1.0" +authors = ["Dioxus Labs"] +edition = "2021" +publish = false + +[dependencies] +dioxus = { workspace = true, features = ["fullstack", "router"] } +reqwest = { workspace = true, features = ["json"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +rusqlite = { version = "0.32.0", optional = true } + +[features] +default = ["web"] +web = ["dioxus/web"] +desktop = ["dioxus/desktop"] +mobile = ["dioxus/mobile"] +server = ["dioxus/server", "dep:rusqlite"] diff --git a/example-projects/hotdog/Dioxus.toml b/example-projects/hotdog/Dioxus.toml new file mode 100644 index 000000000..cd5eb3b74 --- /dev/null +++ b/example-projects/hotdog/Dioxus.toml @@ -0,0 +1,8 @@ +[application] + +# App (Project) Name +name = "hot_dog" + +[bundle] +identifier = "com.dioxuslabs" +publisher = "Dioxus Labs" diff --git a/example-projects/hotdog/Dockerfile b/example-projects/hotdog/Dockerfile new file mode 100644 index 000000000..897ce6492 --- /dev/null +++ b/example-projects/hotdog/Dockerfile @@ -0,0 +1,24 @@ +FROM rust:1 AS chef +RUN cargo install cargo-chef +WORKDIR /app + +FROM chef AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +FROM chef AS builder +COPY --from=planner /app/recipe.json recipe.json +RUN cargo chef cook --release --recipe-path recipe.json +COPY . . +RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/DioxusLabs/dioxus/refs/heads/main/.github/install.sh | bash +RUN /.cargo/bin/dx bundle --platform web + +FROM chef AS runtime +COPY --from=builder /app/target/dx/hotdog/release/web/ /usr/local/app + +ENV PORT=8080 +ENV IP=0.0.0.0 +EXPOSE 8080 + +WORKDIR /usr/local/app +ENTRYPOINT [ "/usr/local/app/server" ] diff --git a/example-projects/hotdog/README.md b/example-projects/hotdog/README.md new file mode 100644 index 000000000..ee181538b --- /dev/null +++ b/example-projects/hotdog/README.md @@ -0,0 +1,16 @@ +# Hot diggity dog! + +A Dioxus demo app for the new tutorial! + +![Demo](/assets/screenshot.png) + +## To run + +Make sure you cd to this directory (dioxus/hotdog) and then `serve` any platform: + +```rust +dx serve --platform web +dx serve --platform desktop +dx serve --platform ios +dx serve --platform android +``` diff --git a/example-projects/hotdog/assets/favicon.ico b/example-projects/hotdog/assets/favicon.ico new file mode 100644 index 000000000..eed0c0973 Binary files /dev/null and b/example-projects/hotdog/assets/favicon.ico differ diff --git a/example-projects/hotdog/assets/header.svg b/example-projects/hotdog/assets/header.svg new file mode 100644 index 000000000..59c96f2f2 --- /dev/null +++ b/example-projects/hotdog/assets/header.svg @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/example-projects/hotdog/assets/main.css b/example-projects/hotdog/assets/main.css new file mode 100644 index 000000000..c167859de --- /dev/null +++ b/example-projects/hotdog/assets/main.css @@ -0,0 +1,149 @@ +/* App-wide styling */ +html, body { + background-color: #0e0e0e; + color: white; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + height: 100%; + width: 100%; + overflow: hidden; + margin: 0; +} + +#main { + display: flex; + flex-direction: column; + height: 100%; + justify-content: space-between; +} + +#dogview { + max-height: 80vh; + flex-grow: 1; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +#dogimg { + display: block; + max-width: 50%; + max-height: 50%; + transform: scale(1.8); + border-radius: 5px; + border: 1px solid rgb(233, 233, 233); + box-shadow: 0px 0px 5px 1px rgb(216, 216, 216, 0.5); +} + +#title { + text-align: center; + padding-top: 10px; + border-bottom: 1px solid #a8a8a8; + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: center; +} + +#title a { + text-decoration: none; + color: white; +} + +#heart { + background-color: white; + padding: 5px; + border-radius: 5px; +} + +#title span { + width: 20px; +} + +#title h1 { + margin: 0.25em; + font-style: italic; +} + +#buttons { + display: flex; + flex-direction: row; + justify-content: center; + gap: 20px; + /* padding-top: 20px; */ + padding-bottom: 20px; +} + +#skip { background-color: gray } +#save { background-color: green; } + +#skip, #save { + padding: 5px 30px 5px 30px; + border-radius: 3px; + font-size: 2rem; + font-weight: bold; + color: rgb(230, 230, 230) +} + +#navbar { + border: 1px solid rgb(233, 233, 233); + border-width: 1px 0px 0px 0px; + display: flex; + flex-direction: row; + justify-content: space-evenly; + padding: 20px; + gap: 20px; +} + +#navbar a { + background-color: #a8a8a8; + border-radius: 5px; + border: 1px solid black; + text-decoration: none; + color: black; + padding: 10px 30px 10px 30px; +} + +#favorites { + flex-grow: 1; + overflow: hidden; + display: flex; + flex-direction: column; + padding: 10px; +} + +#favorites-container { + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + gap: 10px; + padding: 10px; +} + +.favorite-dog { + max-height: 180px; + max-width: 60%; + position: relative; +} + +.favorite-dog img { + max-height: 150px; + border-radius: 5px; + margin: 5px; +} + +.favorite-dog:hover button { + display: block; +} + +.favorite-dog button { + display: none; + position: absolute; + bottom: 10px; + left: 10px; + z-index: 10; +} diff --git a/example-projects/hotdog/assets/screenshot.png b/example-projects/hotdog/assets/screenshot.png new file mode 100644 index 000000000..83e6060fe Binary files /dev/null and b/example-projects/hotdog/assets/screenshot.png differ diff --git a/example-projects/hotdog/fly.toml b/example-projects/hotdog/fly.toml new file mode 100644 index 000000000..16e259823 --- /dev/null +++ b/example-projects/hotdog/fly.toml @@ -0,0 +1,26 @@ +# fly.toml app configuration file generated for hot-dog on 2024-12-19T18:23:45-08:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'hot-dog' +primary_region = 'sjc' + +[build] + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = 'stop' + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[vm]] + memory = '1gb' + cpu_kind = 'shared' + cpus = 1 + +[mounts] + source = "hotdogdb" + destination = "/usr/local/app/hotdogdb" diff --git a/example-projects/hotdog/src/backend.rs b/example-projects/hotdog/src/backend.rs new file mode 100644 index 000000000..8c7f9245d --- /dev/null +++ b/example-projects/hotdog/src/backend.rs @@ -0,0 +1,43 @@ +use dioxus::prelude::*; + +#[cfg(feature = "server")] +thread_local! { + static DB: rusqlite::Connection = { + let conn = rusqlite::Connection::open("hotdogdb/hotdog.db").expect("Failed to open database"); + + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS dogs ( + id INTEGER PRIMARY KEY, + url TEXT NOT NULL + );", + ).unwrap(); + + conn + }; +} + +#[server(endpoint = "list_dogs")] +pub async fn list_dogs() -> Result, ServerFnError> { + let dogs = DB.with(|f| { + f.prepare("SELECT id, url FROM dogs ORDER BY id DESC LIMIT 10") + .unwrap() + .query_map([], |row| Ok((row.get(0)?, row.get(1)?))) + .unwrap() + .map(|r| r.unwrap()) + .collect() + }); + + Ok(dogs) +} + +#[server(endpoint = "remove_dog")] +pub async fn remove_dog(id: usize) -> Result<(), ServerFnError> { + DB.with(|f| f.execute("DELETE FROM dogs WHERE id = ?1", [&id]))?; + Ok(()) +} + +#[server(endpoint = "save_dog")] +pub async fn save_dog(image: String) -> Result<(), ServerFnError> { + _ = DB.with(|f| f.execute("INSERT INTO dogs (url) VALUES (?1)", [&image])); + Ok(()) +} diff --git a/example-projects/hotdog/src/components/favorites.rs b/example-projects/hotdog/src/components/favorites.rs new file mode 100644 index 000000000..afae04396 --- /dev/null +++ b/example-projects/hotdog/src/components/favorites.rs @@ -0,0 +1,25 @@ +use dioxus::prelude::*; + +#[component] +pub fn Favorites() -> Element { + let mut favorites = use_resource(crate::backend::list_dogs); + + rsx! { + div { id: "favorites", + div { id: "favorites-container", + for (id , url) in favorites.suspend()?.cloned().unwrap() { + div { class: "favorite-dog", key: "{id}", + img { src: "{url}" } + button { + onclick: move |_| async move { + crate::backend::remove_dog(id).await.unwrap(); + favorites.restart(); + }, + "❌" + } + } + } + } + } + } +} diff --git a/example-projects/hotdog/src/components/mod.rs b/example-projects/hotdog/src/components/mod.rs new file mode 100644 index 000000000..a3f4b203b --- /dev/null +++ b/example-projects/hotdog/src/components/mod.rs @@ -0,0 +1,7 @@ +mod favorites; +mod nav; +mod view; + +pub use favorites::*; +pub use nav::*; +pub use view::*; diff --git a/example-projects/hotdog/src/components/nav.rs b/example-projects/hotdog/src/components/nav.rs new file mode 100644 index 000000000..d0eee87e6 --- /dev/null +++ b/example-projects/hotdog/src/components/nav.rs @@ -0,0 +1,19 @@ +use crate::Route; +use dioxus::prelude::*; + +#[component] +pub fn NavBar() -> Element { + rsx! { + div { id: "title", + span {} + + Link { to: Route::DogView, + h1 { "🌭 HotDog! " } + } + + Link { to: Route::Favorites, id: "heart", "♥️" } + } + + Outlet:: {} + } +} diff --git a/example-projects/hotdog/src/components/view.rs b/example-projects/hotdog/src/components/view.rs new file mode 100644 index 000000000..3cfa11171 --- /dev/null +++ b/example-projects/hotdog/src/components/view.rs @@ -0,0 +1,42 @@ +use dioxus::prelude::*; + +#[component] +pub fn DogView() -> Element { + let mut img_src = use_resource(|| async move { + #[derive(serde::Deserialize)] + struct DogApi { + message: String, + } + + reqwest::get("https://dog.ceo/api/breeds/image/random") + .await + .unwrap() + .json::() + .await + .unwrap() + .message + }); + + rsx! { + div { id: "dogview", + img { id: "dogimg", src: "{img_src().unwrap_or_default()}" } + } + div { id: "buttons", + button { + id: "skip", + onclick: move |_| { + img_src.restart(); + }, + "skip" + } + button { + id: "save", + onclick: move |_| async move { + img_src.restart(); + crate::backend::save_dog(img_src().unwrap()).await.unwrap(); + }, + "save!" + } + } + } +} diff --git a/example-projects/hotdog/src/main.rs b/example-projects/hotdog/src/main.rs new file mode 100644 index 000000000..4164416f5 --- /dev/null +++ b/example-projects/hotdog/src/main.rs @@ -0,0 +1,29 @@ +mod backend; +mod components; + +use components::{DogView, Favorites, NavBar}; +use dioxus::prelude::*; + +#[derive(Routable, PartialEq, Clone)] +enum Route { + #[layout(NavBar)] + #[route("/")] + DogView, + + #[route("/favorites")] + Favorites, +} + +fn main() { + #[cfg(not(feature = "server"))] + server_fn::client::set_server_url("https://hot-dog.fly.dev"); + + dioxus::launch(app); +} + +fn app() -> Element { + rsx! { + document::Stylesheet { href: asset!("/assets/main.css") } + Router:: {} + } +} diff --git a/notes/RELEASING.md b/notes/RELEASING.md index 0180c6493..a871824a7 100644 --- a/notes/RELEASING.md +++ b/notes/RELEASING.md @@ -103,7 +103,7 @@ There are a number of crates that are part of the Dioxus ecosystem that we usual Verify everything works and is ready to go. - [ ] crates.io is up to date -- [ ] docs.rs is up to date and has no failing builds for major crates (dioxus, dioxus-desktop, dioxus-mobile, etc) +- [ ] docs.rs is up to date and has no failing builds for major crates (dioxus, dioxus-desktop, etc) - [ ] github release post is out - [ ] The relevant GitHub tag is created - [ ] Any fixes are backported to the relevant branches. diff --git a/packages/autofmt/tests/samples/long.rsx b/packages/autofmt/tests/samples/long.rsx index bd0386fb8..038bb8d37 100644 --- a/packages/autofmt/tests/samples/long.rsx +++ b/packages/autofmt/tests/samples/long.rsx @@ -1,13 +1,7 @@ use dioxus::prelude::*; #[component] -pub fn Explainer<'a>( - cx: Scope<'a>, - invert: bool, - title: &'static str, - content: Element, - flasher: Element, -) -> Element { +pub fn Explainer(invert: bool, title: &'static str, content: Element, flasher: Element) -> Element { // pt-5 sm:pt-24 lg:pt-24 let mut right = rsx! { diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 277137e34..0b1d141e5 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -149,9 +149,7 @@ built = { version = "0.7.5", features = ["git2"] } [features] default = [] -plugin = [] tokio-console = ["dep:console-subscriber", "tokio/tracing"] -bundle = [] no-downloads = [] [[bin]] diff --git a/packages/cli/src/build/assets.rs b/packages/cli/src/build/assets.rs index 96faf4d04..79f10bbaa 100644 --- a/packages/cli/src/build/assets.rs +++ b/packages/cli/src/build/assets.rs @@ -34,7 +34,7 @@ use std::{ }; use crate::Result; -use anyhow::Context; +use anyhow::{bail, Context}; use const_serialize::{ConstVec, SerializeConst}; use dioxus_cli_opt::AssetManifest; use manganis::BundledAsset; @@ -352,11 +352,10 @@ pub(crate) fn extract_assets_from_file(path: impl AsRef) -> Result Result { + pub(crate) fn new(request: &BuildRequest) -> Result { let (tx, rx) = futures_channel::mpsc::unbounded(); Ok(Self { build: request.clone(), stage: BuildStage::Initializing, - build_task: tokio::spawn({ - let request = request.clone(); - let tx = tx.clone(); - async move { - let ctx = BuildContext { - mode, - tx: tx.clone(), - }; - request.verify_tooling(&ctx).await?; - request.prepare_build_dir()?; - request.build(&ctx).await - } - }), + build_task: tokio::task::spawn(std::future::pending()), tx, rx, patches: vec![], @@ -164,6 +152,29 @@ impl AppBuilder { }) } + /// Create a new `AppBuilder` and immediately start a build process. + pub fn started(request: &BuildRequest, mode: BuildMode) -> Result { + let mut builder = Self::new(request)?; + builder.start(mode); + Ok(builder) + } + + pub(crate) fn start(&mut self, mode: BuildMode) { + self.build_task = tokio::spawn({ + let request = self.build.clone(); + let tx = self.tx.clone(); + async move { + let ctx = BuildContext { + mode, + tx: tx.clone(), + }; + request.verify_tooling(&ctx).await?; + request.prepare_build_dir()?; + request.build(&ctx).await + } + }); + } + /// Wait for any new updates to the builder - either it completed or gave us a message etc pub(crate) async fn wait(&mut self) -> BuilderUpdate { use futures_util::StreamExt; @@ -178,7 +189,7 @@ impl AppBuilder { match bundle { Ok(Ok(bundle)) => BuilderUpdate::BuildReady { bundle }, Ok(Err(err)) => BuilderUpdate::BuildFailed { err }, - Err(err) => BuilderUpdate::BuildFailed { err: crate::Error::Runtime(format!("Build panicked! {:#?}", err)) }, + Err(err) => BuilderUpdate::BuildFailed { err: anyhow::anyhow!("Build panicked! {:#?}", err) }, } }, Some(Ok(Some(msg))) = OptionFuture::from(self.stdout.as_mut().map(|f| f.next_line())) => { @@ -596,8 +607,12 @@ impl AppBuilder { let triple = self.build.triple.clone(); let asset_dir = self.build.asset_dir(); + // Hotpatch asset!() calls for bundled in res.assets.assets() { - let original_artifacts = self.artifacts.as_mut().unwrap(); + let original_artifacts = self + .artifacts + .as_mut() + .context("No artifacts to hotpatch")?; if original_artifacts.assets.contains(bundled) { continue; @@ -623,6 +638,18 @@ impl AppBuilder { } } + // Make sure to add `include!()` calls to the watcher so we can watch changes as they evolve + for file in res.depinfo.files.iter() { + let original_artifacts = self + .artifacts + .as_mut() + .context("No artifacts to hotpatch")?; + + if !original_artifacts.depinfo.files.contains(file) { + original_artifacts.depinfo.files.push(file.clone()); + } + } + tracing::debug!("Patching {} -> {}", original.display(), new.display()); let mut jump_table = crate::build::create_jump_table(&new, &triple, cache)?; @@ -891,7 +918,7 @@ impl AppBuilder { .context("Failed to parse xcrun output")?; let device_uuid = json["result"]["devices"][0]["identifier"] .as_str() - .ok_or("Failed to extract device UUID")? + .context("Failed to extract device UUID")? .to_string(); Ok(device_uuid) @@ -918,14 +945,14 @@ impl AppBuilder { .await?; if !output.status.success() { - return Err(format!("Failed to install app: {:?}", output).into()); + bail!("Failed to install app: {:?}", output); } let json: Value = serde_json::from_str(&std::fs::read_to_string(tmpfile.path())?) .context("Failed to parse xcrun output")?; let installation_url = json["result"]["installedApplications"][0]["installationURL"] .as_str() - .ok_or("Failed to extract installation URL")? + .context("Failed to extract installation URL from xcrun output")? .to_string(); Ok(installation_url) @@ -953,7 +980,7 @@ impl AppBuilder { .await?; if !output.status.success() { - return Err(format!("Failed to launch app: {:?}", output).into()); + bail!("Failed to launch app: {:?}", output); } let json: Value = serde_json::from_str(&std::fs::read_to_string(tmpfile.path())?) @@ -961,7 +988,7 @@ impl AppBuilder { let status_pid = json["result"]["process"]["processIdentifier"] .as_u64() - .ok_or("Failed to extract process identifier")?; + .context("Failed to extract process identifier")?; let output = Command::new("xcrun") .args([ @@ -978,7 +1005,7 @@ impl AppBuilder { .await?; if !output.status.success() { - return Err(format!("Failed to resume app: {:?}", output).into()); + bail!("Failed to resume app: {:?}", output); } Ok(()) @@ -998,18 +1025,27 @@ To accept the agreement, go to https://developer.apple.com/account To create a provisioning profile, follow the instructions here: https://developer.apple.com/documentation/xcode/sharing-your-teams-signing-certificates"#; - let profiles_folder = dirs::home_dir() + // Check the xcode 16 location first + let mut profiles_folder = dirs::home_dir() .context("Your machine has no home-dir")? .join("Library/Developer/Xcode/UserData/Provisioning Profiles"); + // If it doesn't exist, check the old location + if !profiles_folder.exists() { + profiles_folder = dirs::home_dir() + .context("Your machine has no home-dir")? + .join("Library/MobileDevice/Provisioning Profiles"); + } + if !profiles_folder.exists() || profiles_folder.read_dir()?.next().is_none() { tracing::error!( r#"No provisioning profiles found when trying to codesign the app. -We checked the folder: {} +We checked the folders: +- XCode16: ~/Library/Developer/Xcode/UserData/Provisioning Profiles +- XCode15: ~/Library/MobileDevice/Provisioning Profiles {CODESIGN_ERROR} -"#, - profiles_folder.display() +"# ) } @@ -1125,8 +1161,10 @@ We checked the folder: {} .context("Failed to codesign the app")?; if !output.status.success() { - let stderr = String::from_utf8(output.stderr).unwrap_or_default(); - return Err(format!("Failed to codesign the app: {stderr}").into()); + bail!( + "Failed to codesign the app: {}", + String::from_utf8(output.stderr).unwrap_or_default() + ); } Ok(()) diff --git a/packages/cli/src/build/context.rs b/packages/cli/src/build/context.rs index e38ec12c0..db99ac81d 100644 --- a/packages/cli/src/build/context.rs +++ b/packages/cli/src/build/context.rs @@ -105,7 +105,7 @@ impl BuildContext { } pub(crate) fn status_build_error(&self, line: String) { - tracing::error!(dx_src = ?TraceSrc::Cargo, "{line}"); + tracing::debug!(dx_src = ?TraceSrc::Cargo, "{line}"); } pub(crate) fn status_build_message(&self, line: String) { diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index 3d9965194..49b5dbd61 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -320,8 +320,9 @@ use crate::{ RustcArgs, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR, }; -use anyhow::Context; +use anyhow::{bail, Context}; use cargo_metadata::diagnostic::Diagnostic; +use depinfo::RustcDepInfo; use dioxus_cli_config::format_base_path_meta_element; use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV}; use dioxus_cli_opt::{process_file_to, AssetManifest}; @@ -331,7 +332,7 @@ use manganis::AssetOptions; use manganis_core::AssetVariant; use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; -use std::borrow::Cow; +use std::{borrow::Cow, ffi::OsString}; use std::{ collections::{BTreeMap, HashSet}, io::Write, @@ -391,6 +392,7 @@ pub(crate) struct BuildRequest { pub(crate) link_args_file: Arc, pub(crate) link_err_file: Arc, pub(crate) rustc_wrapper_args_file: Arc, + pub(crate) command_file: Arc, pub(crate) base_path: Option, pub(crate) using_dioxus_explicitly: bool, } @@ -443,6 +445,7 @@ pub struct BuildArtifacts { pub(crate) assets: AssetManifest, pub(crate) mode: BuildMode, pub(crate) patch_cache: Option>, + pub(crate) depinfo: RustcDepInfo, } impl BuildRequest { @@ -569,13 +572,10 @@ impl BuildRequest { }, None if !using_dioxus_explicitly => Platform::autodetect_from_cargo_feature("desktop").unwrap(), None => match enabled_platforms.len() { - 0 => return Err(anyhow::anyhow!("No platform specified and no platform marked as default in Cargo.toml. Try specifying a platform with `--platform`").into()), + 0 => bail!("No platform specified and no platform marked as default in Cargo.toml. Try specifying a platform with `--platform`"), 1 => enabled_platforms[0], _ => { - return Err(anyhow::anyhow!( - "Multiple platforms enabled in Cargo.toml. Please specify a platform with `--platform` or set a default platform in Cargo.toml" - ) - .into()) + bail!("Multiple platforms enabled in Cargo.toml. Please specify a platform with `--platform` or set a default platform in Cargo.toml") } }, }; @@ -662,7 +662,13 @@ impl BuildRequest { "-Clink-arg=-landroid".to_string(), "-Clink-arg=-llog".to_string(), "-Clink-arg=-lOpenSLES".to_string(), + "-Clink-arg=-lc++".to_string(), + "-Clink-arg=-lc++abi".to_string(), "-Clink-arg=-Wl,--export-dynamic".to_string(), + format!( + "-Clink-arg=-Wl,--sysroot={}", + workspace.android_tools()?.sysroot().display() + ), ]); } @@ -712,6 +718,9 @@ impl BuildRequest { let session_cache_dir = Arc::new( TempDir::new().context("Failed to create temporary directory for session cache")?, ); + let command_file = Arc::new( + NamedTempFile::new().context("Failed to create temporary file for linker args")?, + ); let extra_rustc_args = shell_words::split(&args.rustc_args.clone().unwrap_or_default()) .context("Failed to parse rustc args")?; @@ -752,6 +761,7 @@ impl BuildRequest { custom_linker, link_args_file, link_err_file, + command_file, session_cache_dir, rustc_wrapper_args_file, extra_rustc_args, @@ -774,6 +784,9 @@ impl BuildRequest { // We need to make sure to not react to this though, so the filemap must cache it _ = self.bust_fingerprint(ctx); + // Run any pre-build steps like tailwind, etc + self.prebuild().await?; + // Run the cargo build to produce our artifacts let mut artifacts = self.cargo_build(ctx).await?; @@ -793,12 +806,23 @@ impl BuildRequest { ctx.status_start_bundle(); self.write_executable(ctx, &artifacts.exe, &mut artifacts.assets) - .await?; - self.write_frameworks(ctx, &artifacts.direct_rustc).await?; - self.write_assets(ctx, &artifacts.assets).await?; - self.write_metadata().await?; - self.optimize(ctx).await?; - self.assemble(ctx).await?; + .await + .context("Failed to write executable")?; + self.write_frameworks(ctx, &artifacts.direct_rustc) + .await + .context("Failed to write frameworks")?; + self.write_assets(ctx, &artifacts.assets) + .await + .context("Failed to write assets")?; + self.write_metadata() + .await + .context("Failed to write metadata")?; + self.optimize(ctx) + .await + .context("Failed to optimize build")?; + self.assemble(ctx) + .await + .context("Failed to assemble build")?; tracing::debug!("Bundle created at {}", self.root_dir().display()); } @@ -916,10 +940,7 @@ impl BuildRequest { // since that is a really bad user experience. Message::BuildFinished(finished) => { if !finished.success { - return Err(anyhow::anyhow!( - "Cargo build failed, signaled by the compiler. Toggle tracing mode (press `t`) for more information." - ) - .into()); + bail!("cargo build finished with errors.") } } _ => {} @@ -971,6 +992,7 @@ impl BuildRequest { let time_end = SystemTime::now(); let mode = ctx.mode.clone(); let platform = self.platform; + let depinfo = RustcDepInfo::from_file(&exe.with_extension("d")).unwrap_or_default(); tracing::debug!( "Build completed successfully in {}us: {:?}", @@ -986,6 +1008,7 @@ impl BuildRequest { time_start, assets, mode, + depinfo, patch_cache: None, }) } @@ -1079,6 +1102,7 @@ impl BuildRequest { // todo - how do we handle windows dlls? we don't want to bundle the system dlls // for now, we don't do anything with dlls, and only use .dylibs and .so files + // Write dylibs and dlls to the frameworks folder if arg.ends_with(".dylib") | arg.ends_with(".so") { let from = PathBuf::from(arg); let to = framework_dir.join(from.file_name().unwrap()); @@ -1104,6 +1128,16 @@ impl BuildRequest { std::fs::copy(from, to)?; } } + + // On android, the c++_static flag means we need to copy the libc++_shared.so precompiled + // library to the jniLibs folder + if arg.contains("-lc++_static") && self.platform == Platform::Android { + std::fs::copy( + self.workspace.android_tools()?.libcpp_shared(&self.triple), + framework_dir.join("libc++_shared.so"), + ) + .with_context(|| "Failed to copy libc++_shared.so into bundle")?; + } } Ok(()) @@ -1115,6 +1149,13 @@ impl BuildRequest { self.root_dir().join("Contents").join("Frameworks") } OperatingSystem::IOS(_) => self.root_dir().join("Frameworks"), + OperatingSystem::Linux if self.platform == Platform::Android => self + .root_dir() + .join("app") + .join("src") + .join("main") + .join("jniLibs") + .join("arm64-v8a"), OperatingSystem::Linux | OperatingSystem::Windows => self.root_dir(), _ => self.root_dir(), } @@ -1129,14 +1170,6 @@ impl BuildRequest { return Ok(()); } - // Run the tailwind build before bundling anything else - crate::TailwindCli::run_once( - self.package_manifest_dir(), - self.config.application.tailwind_input.clone(), - self.config.application.tailwind_output.clone(), - ) - .await?; - let asset_dir = self.asset_dir(); // First, clear the asset dir of any files that don't exist in the new manifest @@ -1379,15 +1412,28 @@ impl BuildRequest { tracing::trace!("Linking with {:?} using args: {:#?}", linker, object_files); + let mut out_args: Vec = vec![]; + out_args.extend(object_files.iter().map(Into::into)); + out_args.extend(dylibs.iter().map(Into::into)); + out_args.extend(self.thin_link_args(&args)?.iter().map(Into::into)); + out_args.extend(out_arg.iter().map(Into::into)); + + if cfg!(windows) { + let cmd_contents: String = out_args + .iter() + .map(|s| format!("\"{}\"", s.to_string_lossy())) + .join(" "); + std::fs::write(self.command_file.path(), cmd_contents) + .context("Failed to write linker command file")?; + out_args = vec![format!("@{}", self.command_file.path().display()).into()]; + } + // Run the linker directly! // // We dump its output directly into the patch exe location which is different than how rustc // does it since it uses llvm-objcopy into the `target/debug/` folder. let res = Command::new(linker) - .args(object_files.iter()) - .args(dylibs.iter()) - .args(self.thin_link_args(&args)?) - .args(out_arg) + .args(out_args) .env_clear() .envs(rustc_args.envs.iter().map(|(k, v)| (k, v))) .output() @@ -1561,7 +1607,7 @@ impl BuildRequest { } LinkerFlavor::Unsupported => { - return Err(anyhow::anyhow!("Unsupported platform for thin linking").into()) + bail!("Unsupported platform for thin linking") } } @@ -1791,7 +1837,7 @@ impl BuildRequest { // And then remove the rest of the rlibs // // We also need to insert the -force_load flag to force the linker to load the archive - let mut args = rustc_args.link_args.clone(); + let mut args: Vec<_> = rustc_args.link_args.iter().skip(1).cloned().collect(); if let Some(last_object) = args.iter().rposition(|arg| arg.ends_with(".o")) { if archive_has_contents { match self.linker_flavor() { @@ -1879,21 +1925,33 @@ impl BuildRequest { args.remove(flavor_idx); } + // Set the output file + match self.triple.operating_system { + OperatingSystem::Windows => args.push(format!("/OUT:{}", exe.display())), + _ => args.extend(["-o".to_string(), exe.display().to_string()]), + } + // And now we can run the linker with our new args let linker = self.select_linker()?; tracing::trace!("Fat linking with args: {:?} {:#?}", linker, args); - tracing::trace!("Fat linking with env: {:#?}", rustc_args.envs); + tracing::trace!("Fat linking with env:"); + for e in rustc_args.envs.iter() { + tracing::trace!(" {}={}", e.0, e.1); + } + + // Handle windows command files + let mut out_args = args.clone(); + if cfg!(windows) { + let cmd_contents: String = out_args.iter().map(|f| format!("\"{f}\"")).join(" "); + std::fs::write(self.command_file.path(), cmd_contents) + .context("Failed to write linker command file")?; + out_args = vec![format!("@{}", self.command_file.path().display())]; + } // Run the linker directly! - let out_arg = match self.triple.operating_system { - OperatingSystem::Windows => vec![format!("/OUT:{}", exe.display())], - _ => vec!["-o".to_string(), exe.display().to_string()], - }; - let res = Command::new(linker) - .args(args.iter().skip(1)) - .args(out_arg) + .args(out_args) .env_clear() .envs(rustc_args.envs.iter().map(|(k, v)| (k, v))) .output() @@ -2062,12 +2120,8 @@ impl BuildRequest { cmd.arg("-Crelocation-model=pic"); } - tracing::debug!("Direct rustc: {:#?}", cmd); - cmd.envs(rustc_args.envs.iter().cloned()); - // tracing::trace!("Setting env vars: {:#?}", rustc_args.envs); - Ok(cmd) } @@ -2084,16 +2138,24 @@ impl BuildRequest { _ => { let mut cmd = Command::new("cargo"); + let env = self.cargo_build_env_vars(ctx)?; + let args = self.cargo_build_arguments(ctx); + + tracing::trace!("Building with cargo rustc"); + for e in env.iter() { + tracing::trace!(": {}={}", e.0, e.1); + } + + for a in args.iter() { + tracing::trace!(": {}", a); + } + cmd.arg("rustc") .current_dir(self.crate_dir()) .arg("--message-format") .arg("json-diagnostic-rendered-ansi") - .args(self.cargo_build_arguments(ctx)) - .envs( - self.cargo_build_env_vars(ctx)? - .iter() - .map(|(k, v)| (k.as_ref(), v)), - ); + .args(args) + .envs(env.iter().map(|(k, v)| (k.as_ref(), v))); if ctx.mode == BuildMode::Fat { cmd.env( @@ -2109,8 +2171,6 @@ impl BuildRequest { ); } - tracing::debug!("Cargo: {:#?}", cmd); - Ok(cmd) } } @@ -2351,7 +2411,67 @@ impl BuildRequest { Ok(env_vars) } + /// Set the environment variables required for building on Android. + /// + /// This involves setting sysroots, CC, CXX, AR, and other environment variables along with + /// vars that cc-rs uses for its C/C++ compilation. + /// + /// We pulled the environment setup from `cargo ndk` and attempt to mimic its behavior to retain + /// compatibility with existing crates that work with `cargo ndk`. + /// + /// + /// + /// cargo-ndk is MIT licensed. + /// + /// fn android_env_vars(&self) -> Result, String)>> { + // Derived from getenv_with_target_prefixes in `cc` crate. + fn cc_env(var_base: &str, triple: &str) -> (String, Option) { + #[inline] + fn env_var_with_key(key: String) -> Option<(String, String)> { + std::env::var(&key).map(|value| (key, value)).ok() + } + + let triple_u = triple.replace('-', "_"); + let most_specific_key = format!("{}_{}", var_base, triple); + + env_var_with_key(most_specific_key.to_string()) + .or_else(|| env_var_with_key(format!("{}_{}", var_base, triple_u))) + .or_else(|| env_var_with_key(format!("TARGET_{}", var_base))) + .or_else(|| env_var_with_key(var_base.to_string())) + .map(|(key, value)| (key, Some(value))) + .unwrap_or_else(|| (most_specific_key, None)) + } + + fn cargo_env_target_cfg(triple: &str, key: &str) -> String { + format!("CARGO_TARGET_{}_{}", &triple.replace('-', "_"), key).to_uppercase() + } + + fn clang_target(rust_target: &str, api_level: u8) -> String { + let target = match rust_target { + "arm-linux-androideabi" => "armv7a-linux-androideabi", + "armv7-linux-androideabi" => "armv7a-linux-androideabi", + _ => rust_target, + }; + format!("--target={target}{api_level}") + } + + fn sysroot_target(rust_target: &str) -> &str { + (match rust_target { + "armv7-linux-androideabi" => "arm-linux-androideabi", + _ => rust_target, + }) as _ + } + fn rt_builtins(rust_target: &str) -> &str { + (match rust_target { + "armv7-linux-androideabi" => "arm", + "aarch64-linux-android" => "aarch64", + "i686-linux-android" => "i686", + "x86_64-linux-android" => "x86_64", + _ => rust_target, + }) as _ + } + let mut env_vars: Vec<(Cow<'static, str>, String)> = vec![]; let tools = self.workspace.android_tools()?; @@ -2361,7 +2481,7 @@ impl BuildRequest { let target_cc = tools.target_cc(); let target_cxx = tools.target_cxx(); let java_home = tools.java_home(); - let ndk = tools.ndk.clone(); + let ndk_home = tools.ndk.clone(); tracing::debug!( r#"Using android: min_sdk_version: {min_sdk_version} @@ -2372,61 +2492,153 @@ impl BuildRequest { java_home: {java_home:?} "# ); - env_vars.push(( - "ANDROID_NATIVE_API_LEVEL".into(), - min_sdk_version.to_string(), - )); - env_vars.push(("TARGET_AR".into(), ar_path.display().to_string())); - env_vars.push(("TARGET_CC".into(), target_cc.display().to_string())); - env_vars.push(("TARGET_CXX".into(), target_cxx.display().to_string())); - env_vars.push(( - format!( - "CARGO_TARGET_{}_LINKER", - self.triple - .to_string() - .to_ascii_uppercase() - .replace("-", "_") - ) - .into(), - linker.display().to_string(), - )); - env_vars.push(("ANDROID_NDK_ROOT".into(), ndk.display().to_string())); if let Some(java_home) = java_home { tracing::debug!("Setting JAVA_HOME to {java_home:?}"); env_vars.push(("JAVA_HOME".into(), java_home.display().to_string())); } - // Set the wry env vars - this is where wry will dump its kotlin files. - // Their setup is really annyoing and requires us to hardcode `dx` to specific versions of tao/wry. - env_vars.push(("WRY_ANDROID_PACKAGE".into(), "dev.dioxus.main".to_string())); - env_vars.push(("WRY_ANDROID_LIBRARY".into(), "dioxusmain".to_string())); - env_vars.push(( - "WRY_ANDROID_KOTLIN_FILES_OUT_DIR".into(), - self.wry_android_kotlin_files_out_dir() - .display() - .to_string(), - )); + let triple = self.triple.to_string(); - // todo(jon): the guide for openssl recommends extending the path to include the tools dir - // in practice I couldn't get this to work, but this might eventually become useful. - // - // https://github.com/openssl/openssl/blob/master/NOTES-ANDROID.md#configuration - // - // They recommend a configuration like this: - // - // // export ANDROID_NDK_ROOT=/home/whoever/Android/android-sdk/ndk/20.0.5594570 - // PATH=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin:$ANDROID_NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin:$PATH - // ./Configure android-arm64 -D__ANDROID_API__=29 - // make - // - // let tools_dir = arch.android_tools_dir(&ndk); - // let extended_path = format!( - // "{}:{}", - // tools_dir.display(), - // std::env::var("PATH").unwrap_or_default() - // ); - // env_vars.push(("PATH", extended_path)); + // Environment variables for the `cc` crate + let (cc_key, _cc_value) = cc_env("CC", &triple); + let (cflags_key, cflags_value) = cc_env("CFLAGS", &triple); + let (cxx_key, _cxx_value) = cc_env("CXX", &triple); + let (cxxflags_key, cxxflags_value) = cc_env("CXXFLAGS", &triple); + let (ar_key, _ar_value) = cc_env("AR", &triple); + let (ranlib_key, _ranlib_value) = cc_env("RANLIB", &triple); + + // Environment variables for cargo + let cargo_ar_key = cargo_env_target_cfg(&triple, "ar"); + let cargo_rust_flags_key = cargo_env_target_cfg(&triple, "rustflags"); + let bindgen_clang_args_key = + format!("BINDGEN_EXTRA_CLANG_ARGS_{}", &triple.replace('-', "_")); + + let clang_target = clang_target(&self.triple.to_string(), min_sdk_version as _); + let target_cc = tools.target_cc(); + let target_cflags = match cflags_value { + Some(v) => format!("{clang_target} {v}"), + None => clang_target.to_string(), + }; + let target_cxx = tools.target_cxx(); + let target_cxxflags = match cxxflags_value { + Some(v) => format!("{clang_target} {v}"), + None => clang_target.to_string(), + }; + let cargo_ndk_sysroot_path_key = "CARGO_NDK_SYSROOT_PATH"; + let cargo_ndk_sysroot_path = tools.sysroot(); + let cargo_ndk_sysroot_target_key = "CARGO_NDK_SYSROOT_TARGET"; + let cargo_ndk_sysroot_target = sysroot_target(&triple); + let cargo_ndk_sysroot_libs_path_key = "CARGO_NDK_SYSROOT_LIBS_PATH"; + let cargo_ndk_sysroot_libs_path = cargo_ndk_sysroot_path + .join("usr") + .join("lib") + .join(cargo_ndk_sysroot_target); + let target_ar = tools.ar_path(); + let target_ranlib = tools.ranlib(); + let clang_folder = tools.clang_folder(); + + // choose the clang target with the highest version + // Should we filter for only numbers? + let clang_builtins_target = std::fs::read_dir(clang_folder) + .expect("Unable to get clang target directory") + .filter_map(|a| a.ok()) + .max_by(|a, b| a.file_name().cmp(&b.file_name())) + .expect("Unable to get clang target") + .path(); + let clang_rt = format!( + "-L{} -lstatic=clang_rt.builtins-{}-android", + clang_builtins_target.join("lib").join("linux").display(), + rt_builtins(&triple) + ); + + let extra_include: String = format!( + "{}/usr/include/{}", + &cargo_ndk_sysroot_path.display(), + &cargo_ndk_sysroot_target + ); + + let bindgen_args = format!( + "--sysroot={} -I{}", + &cargo_ndk_sysroot_path.display(), + extra_include + ); + + for env in [ + (cc_key, target_cc.clone().into_os_string()), + (cflags_key, target_cflags.into()), + (cxx_key, target_cxx.into_os_string()), + (cxxflags_key, target_cxxflags.into()), + (ar_key, target_ar.clone().into()), + (ranlib_key, target_ranlib.into_os_string()), + (cargo_ar_key, target_ar.into_os_string()), + ( + cargo_ndk_sysroot_path_key.to_string(), + cargo_ndk_sysroot_path.clone().into_os_string(), + ), + ( + cargo_ndk_sysroot_libs_path_key.to_string(), + cargo_ndk_sysroot_libs_path.into_os_string(), + ), + ( + cargo_ndk_sysroot_target_key.to_string(), + cargo_ndk_sysroot_target.into(), + ), + (cargo_rust_flags_key, clang_rt.into()), + (bindgen_clang_args_key, bindgen_args.into()), + ( + "ANDROID_NATIVE_API_LEVEL".to_string(), + min_sdk_version.to_string().into(), + ), + ( + format!( + "CARGO_TARGET_{}_LINKER", + self.triple + .to_string() + .to_ascii_uppercase() + .replace("-", "_") + ), + linker.into_os_string(), + ), + ("ANDROID_NDK_ROOT".to_string(), ndk_home.into_os_string()), + // Set the wry env vars - this is where wry will dump its kotlin files. + // Their setup is really annyoing and requires us to hardcode `dx` to specific versions of tao/wry. + ( + "WRY_ANDROID_PACKAGE".to_string(), + "dev.dioxus.main".to_string().into(), + ), + ( + "WRY_ANDROID_LIBRARY".to_string(), + "dioxusmain".to_string().into(), + ), + ( + "WRY_ANDROID_KOTLIN_FILES_OUT_DIR".to_string(), + self.wry_android_kotlin_files_out_dir().into_os_string(), + ), + // Found this through a comment related to bindgen using the wrong clang for cross compiles + // + // https://github.com/rust-lang/rust-bindgen/issues/2962#issuecomment-2438297124 + // + // https://github.com/KyleMayes/clang-sys?tab=readme-ov-file#environment-variables + ("CLANG_PATH".into(), target_cc.with_extension("exe").into()), + ] { + env_vars.push(( + env.0.into(), + env.1 + .to_str() + .expect("Failed to convert env var value to string") + .to_string(), + )); + } + + if std::env::var("MSYSTEM").is_ok() || std::env::var("CYGWIN").is_ok() { + for var in env_vars.iter_mut() { + // Convert windows paths to unix-style paths + // This is a workaround for the fact that the `cc` crate expects unix-style paths + // and will fail if it encounters windows-style paths. + var.1 = var.1.replace('\\', "/"); + } + } Ok(env_vars) } @@ -2465,7 +2677,7 @@ impl BuildRequest { let output = tokio::process::Command::new("cargo") .arg("+nightly") - .arg("build") + .arg("rustc") .arg("--unit-graph") .arg("-Z") .arg("unstable-options") @@ -2479,7 +2691,11 @@ impl BuildRequest { .await?; if !output.status.success() { - return Err(anyhow::anyhow!("Failed to get unit count").into()); + tracing::trace!( + "Failed to get unit count: {}", + String::from_utf8_lossy(&output.stderr) + ); + bail!("Failed to get unit count"); } let output_text = String::from_utf8(output.stdout).context("Failed to get unit count")?; @@ -2911,15 +3127,6 @@ impl BuildRequest { }) } - /// Return the version of the wasm-bindgen crate if it exists - fn wasm_bindgen_version(&self) -> Option { - self.workspace - .krates - .krates_by_name("wasm-bindgen") - .next() - .map(|krate| krate.krate.version.to_string()) - } - /// Return the platforms that are enabled for the package /// /// Ideally only one platform is enabled but we need to be able to @@ -3224,6 +3431,7 @@ impl BuildRequest { let post_bindgen_wasm = self.wasm_bindgen_wasm_output_file(); let should_bundle_split: bool = self.wasm_split; let bindgen_version = self + .workspace .wasm_bindgen_version() .expect("this should have been checked by tool verification"); @@ -3291,10 +3499,7 @@ impl BuildRequest { ctx.status_splitting_bundle(); if !will_wasm_opt { - return Err(anyhow::anyhow!( - "Bundle splitting should automatically enable wasm-opt, but it was not enabled." - ) - .into()); + bail!("Bundle splitting should automatically enable wasm-opt, but it was not enabled."); } // Load the contents of these binaries since we need both of them @@ -3549,7 +3754,7 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ }, ) .map_err(|e| e.into()), - _ => Err(anyhow::anyhow!("Unsupported platform for Info.plist").into()), + _ => Err(anyhow::anyhow!("Unsupported platform for Info.plist")), } } @@ -3574,7 +3779,10 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ .await?; if !output.status.success() { - return Err(anyhow::anyhow!("Failed to assemble apk: {output:?}").into()); + bail!( + "Failed to assemble apk: {}", + String::from_utf8_lossy(&output.stderr) + ); } } @@ -3593,7 +3801,10 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ .context("Failed to run gradle bundleRelease")?; if !output.status.success() { - return Err(anyhow::anyhow!("Failed to bundleRelease: {output:?}").into()); + bail!( + "Failed to bundleRelease: {}", + String::from_utf8_lossy(&output.stderr) + ); } let app_release = self @@ -3688,7 +3899,7 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ }); if let Err(e) = success.as_ref() { - return Err(format!("Failed to initialize build directory: {e}").into()); + bail!("Failed to initialize build directory: {e}"); } Ok(()) @@ -3816,15 +4027,16 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ // Ensure target is installed. if !self.workspace.has_wasm32_unknown_unknown() { - return Err(Error::Other(anyhow::anyhow!( - "Missing target wasm32-unknown-unknown." - ))); + bail!("Missing target wasm32-unknown-unknown."); } // Wasm bindgen - let krate_bindgen_version = self.wasm_bindgen_version().ok_or(anyhow::anyhow!( - "failed to detect wasm-bindgen version, unable to proceed" - ))?; + let krate_bindgen_version = + self.workspace + .wasm_bindgen_version() + .ok_or(anyhow::anyhow!( + "failed to detect wasm-bindgen version, unable to proceed" + ))?; WasmBindgen::verify_install(&krate_bindgen_version).await?; @@ -3884,9 +4096,9 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ return Ok(()); } - Err(anyhow::anyhow!( - "Android linker not found. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation." - ).into()) + bail!( + "Android linker not found at {linker:?}. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation." + ); } /// Ensure the right dependencies are installed for linux apps. @@ -4347,4 +4559,20 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ .flat_map(|arg| ["--config".to_string(), arg]) .collect() } + + async fn prebuild(&self) -> Result<()> { + if self.platform == Platform::Server { + return Ok(()); + } + + // Run the tailwind build before bundling anything else + crate::TailwindCli::run_once( + self.package_manifest_dir(), + self.config.application.tailwind_input.clone(), + self.config.application.tailwind_output.clone(), + ) + .await?; + + Ok(()) + } } diff --git a/packages/cli/src/build/tools.rs b/packages/cli/src/build/tools.rs index d2db7ee5b..c63cedc9b 100644 --- a/packages/cli/src/build/tools.rs +++ b/packages/cli/src/build/tools.rs @@ -170,6 +170,13 @@ impl AndroidTools { )) } + pub(crate) fn sysroot(&self) -> PathBuf { + // The sysroot is usually located in the NDK under: + // "~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/sysroot" + // or similar, depending on the platform. + self.android_tools_dir().parent().unwrap().join("sysroot") + } + pub(crate) fn sdk(&self) -> PathBuf { // /Users/jonathankelley/Library/Android/sdk/ndk/25.2/... (25.2 is the ndk here) // /Users/jonathankelley/Library/Android/sdk/ @@ -187,6 +194,21 @@ impl AndroidTools { 24 } + pub(crate) fn clang_folder(&self) -> PathBuf { + // The clang folder is usually located in the NDK under: + // "~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/lib/clang/" + // or similar, depending on the platform. + self.android_tools_dir() + .parent() + .unwrap() + .join("lib") + .join("clang") + } + + pub(crate) fn ranlib(&self) -> PathBuf { + self.android_tools_dir().join("llvm-ranlib") + } + pub(crate) fn ar_path(&self) -> PathBuf { self.android_tools_dir().join("llvm-ar") } @@ -261,6 +283,24 @@ impl AndroidTools { triple } + + pub(crate) fn libcpp_shared(&self, triple: &Triple) -> PathBuf { + // The libc++_shared.so is usually located in the sysroot under: + // "~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib//libc++_shared.so" + // or similar, depending on the platform. + self.sysroot() + .join("usr") + .join("lib") + .join(Self::sysroot_target(&triple.to_string())) + .join("libc++_shared.so") + } + + pub(crate) fn sysroot_target(rust_target: &str) -> &str { + (match rust_target { + "armv7-linux-androideabi" => "arm-linux-androideabi", + _ => rust_target, + }) as _ + } } fn var_or_debug(name: &str) -> Option { diff --git a/packages/cli/src/cli/autoformat.rs b/packages/cli/src/cli/autoformat.rs index 41d238af9..cf98726e6 100644 --- a/packages/cli/src/cli/autoformat.rs +++ b/packages/cli/src/cli/autoformat.rs @@ -1,6 +1,6 @@ use super::{check::collect_rs_files, *}; use crate::Workspace; -use anyhow::Context; +use anyhow::{bail, Context}; use dioxus_autofmt::{IndentOptions, IndentType}; use rayon::prelude::*; use std::{borrow::Cow, fs, path::Path}; @@ -54,11 +54,9 @@ impl Autoformat { } else if let Some(raw) = raw { // Format raw text. let indent = indentation_for(".", self.split_line_attributes)?; - if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) { - println!("{}", inner); - } else { - return Err("error formatting codeblock".into()); - } + let formatted = + dioxus_autofmt::fmt_block(&raw, 0, indent).context("error formatting codeblock")?; + println!("{}", formatted); } else { // Default to formatting the project. let crate_dir = if let Some(package) = self.package { @@ -76,11 +74,8 @@ impl Autoformat { Cow::Borrowed(Path::new(".")) }; - if let Err(e) = - autoformat_project(check, split_line_attributes, format_rust_code, crate_dir) - { - return Err(format!("error formatting project: {}", e).into()); - } + autoformat_project(check, split_line_attributes, format_rust_code, crate_dir) + .context("error autoformatting project")?; } Ok(StructuredOutput::Success) @@ -100,19 +95,15 @@ fn refactor_file( } else { fs::read_to_string(&file) }; - let Ok(mut s) = file_content else { - return Err(format!("failed to open file: {}", file_content.unwrap_err()).into()); - }; + let mut s = file_content.context("failed to open file")?; if format_rust_code { s = format_rust(&s)?; } - let Ok(Ok(edits)) = - syn::parse_file(&s).map(|file| dioxus_autofmt::try_fmt_file(&s, &file, indent)) - else { - return Err(format!("failed to format file: {}", s).into()); - }; + let parsed = syn::parse_file(&s).context("failed to parse file")?; + let edits = + dioxus_autofmt::try_fmt_file(&s, &parsed, indent).context("failed to format file")?; let out = dioxus_autofmt::apply_formats(&s, edits); @@ -135,18 +126,16 @@ fn format_file( let mut contents = fs::read_to_string(&path)?; let mut if_write = false; if format_rust_code { - let formatted = format_rust(&contents) - .map_err(|err| Error::Parse(format!("Syntax Error:\n{}", err)))?; + let formatted = format_rust(&contents).context("Syntax Error")?; if contents != formatted { if_write = true; contents = formatted; } } - let parsed = syn::parse_file(&contents) - .map_err(|err| Error::Parse(format!("Failed to parse file: {}", err)))?; + let parsed = syn::parse_file(&contents).context("Failed to parse file")?; let edits = dioxus_autofmt::try_fmt_file(&contents, &parsed, indent) - .map_err(|err| Error::Parse(format!("Failed to format file: {}", err)))?; + .context("Failed to format file")?; let len = edits.len(); if !edits.is_empty() { @@ -202,7 +191,7 @@ fn autoformat_project( let files_formatted: usize = counts.into_iter().flatten().sum(); if files_formatted > 0 && check { - return Err(format!("{} files needed formatting", files_formatted).into()); + bail!("{} files needed formatting", files_formatted); } Ok(()) @@ -220,9 +209,7 @@ fn indentation_for( .output()?; if !out.status.success() { - return Err(Error::Runtime(format!( - "cargo fmt failed with status: {out:?}" - ))); + bail!("cargo fmt failed with status: {out:?}"); } let config = String::from_utf8_lossy(&out.stdout); @@ -232,18 +219,14 @@ fn indentation_for( .find(|line| line.starts_with("hard_tabs ")) .and_then(|line| line.split_once('=')) .map(|(_, value)| value.trim() == "true") - .ok_or_else(|| { - Error::Runtime("Could not find hard_tabs option in rustfmt config".into()) - })?; + .context("Could not find hard_tabs option in rustfmt config")?; let tab_spaces = config .lines() .find(|line| line.starts_with("tab_spaces ")) .and_then(|line| line.split_once('=')) .map(|(_, value)| value.trim().parse::()) - .ok_or_else(|| Error::Runtime("Could not find tab_spaces option in rustfmt config".into()))? - .map_err(|_| { - Error::Runtime("Could not parse tab_spaces option in rustfmt config".into()) - })?; + .context("Could not find tab_spaces option in rustfmt config")? + .context("Could not parse tab_spaces option in rustfmt config")?; Ok(IndentOptions::new( if hard_tabs { @@ -258,7 +241,9 @@ fn indentation_for( /// Format rust code using prettyplease fn format_rust(input: &str) -> Result { - let syntax_tree = syn::parse_file(input).map_err(format_syn_error)?; + let syntax_tree = syn::parse_file(input) + .map_err(format_syn_error) + .context("Failed to parse Rust syntax")?; let output = prettyplease::unparse(&syntax_tree); Ok(output) } @@ -267,10 +252,7 @@ fn format_syn_error(err: syn::Error) -> Error { let start = err.span().start(); let line = start.line; let column = start.column; - Error::Parse(format!( - "Syntax Error in line {} column {}:\n{}", - line, column, err - )) + anyhow::anyhow!("Syntax Error in line {} column {}:\n{}", line, column, err) } #[tokio::test] diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 2400a5258..d57ec62f4 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -70,7 +70,7 @@ impl CommandWithPlatformOverrides { let ssg = self.shared.ssg; let targets = self.into_targets().await?; - AppBuilder::start(&targets.client, BuildMode::Base)? + AppBuilder::started(&targets.client, BuildMode::Base)? .finish_build() .await?; @@ -78,7 +78,7 @@ impl CommandWithPlatformOverrides { if let Some(server) = targets.server.as_ref() { // If the server is present, we need to build it as well - let mut server_build = AppBuilder::start(server, BuildMode::Base)?; + let mut server_build = AppBuilder::started(server, BuildMode::Base)?; server_build.finish_build().await?; // Run SSG and cache static routes diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index d6dd6bbaa..cfa920380 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -1,5 +1,5 @@ use crate::{AppBuilder, BuildArgs, BuildMode, BuildRequest, Platform}; -use anyhow::{anyhow, Context}; +use anyhow::{bail, Context}; use path_absolutize::Absolutize; use std::collections::HashMap; use tauri_bundler::{BundleBinary, BundleSettings, PackageSettings, SettingsBuilder}; @@ -39,7 +39,7 @@ impl Bundle { let BuildTargets { client, server } = self.args.into_targets().await?; - AppBuilder::start(&client, BuildMode::Base)? + AppBuilder::started(&client, BuildMode::Base)? .finish_build() .await?; @@ -47,7 +47,7 @@ impl Bundle { if let Some(server) = server.as_ref() { // If the server is present, we need to build it as well - AppBuilder::start(server, BuildMode::Base)? + AppBuilder::started(server, BuildMode::Base)? .finish_build() .await?; @@ -167,10 +167,10 @@ impl Bundle { // Check if required fields are provided instead of failing silently. if bundle_settings.identifier.is_none() { - return Err(anyhow!("\n\nBundle identifier was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\nidentifier = \"com.mycompany\"\n\n").into()); + bail!("\n\nBundle identifier was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\nidentifier = \"com.mycompany\"\n\n"); } if bundle_settings.publisher.is_none() { - return Err(anyhow!("\n\nBundle publisher was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\npublisher = \"MyCompany\"\n\n").into()); + bail!("\n\nBundle publisher was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\npublisher = \"MyCompany\"\n\n"); } if cfg!(windows) { diff --git a/packages/cli/src/cli/check.rs b/packages/cli/src/cli/check.rs index 4a04fc97a..942e759e6 100644 --- a/packages/cli/src/cli/check.rs +++ b/packages/cli/src/cli/check.rs @@ -5,7 +5,7 @@ use super::*; use crate::BuildRequest; -use anyhow::Context; +use anyhow::{anyhow, Context}; use futures_util::{stream::FuturesUnordered, StreamExt}; use std::path::Path; @@ -118,8 +118,8 @@ async fn check_files_and_report(files_to_check: Vec) -> Result<()> { tracing::info!("No issues found."); Ok(()) } - 1 => Err("1 issue found.".into()), - _ => Err(format!("{} issues found.", total_issues).into()), + 1 => Err(anyhow!("1 issue found.")), + _ => Err(anyhow!("{} issues found.", total_issues)), } } diff --git a/packages/cli/src/cli/clean.rs b/packages/cli/src/cli/clean.rs deleted file mode 100644 index f3c86e58b..000000000 --- a/packages/cli/src/cli/clean.rs +++ /dev/null @@ -1,24 +0,0 @@ -use super::*; -use crate::Result; - -/// Clean build artifacts. -/// -/// Simply runs `cargo clean` -#[derive(Clone, Debug, Parser)] -pub(crate) struct Clean {} - -impl Clean { - /// todo(jon): we should add a config option that just wipes target/dx and target/dioxus-client instead of doing a full clean - pub(crate) async fn clean(self) -> Result { - let output = tokio::process::Command::new("cargo") - .arg("clean") - .output() - .await?; - - if !output.status.success() { - return Err(anyhow::anyhow!("Cargo clean failed.").into()); - } - - Ok(StructuredOutput::Success) - } -} diff --git a/packages/cli/src/cli/create.rs b/packages/cli/src/cli/create.rs index 2251e9a04..7da8bd23e 100644 --- a/packages/cli/src/cli/create.rs +++ b/packages/cli/src/cli/create.rs @@ -1,5 +1,6 @@ use super::*; use crate::TraceSrc; +use anyhow::{bail, Context}; use cargo_generate::{GenerateArgs, TemplatePath, Vcs}; use std::path::Path; @@ -147,9 +148,9 @@ pub(crate) fn name_from_path(path: &Path) -> Result { .absolutize()? .to_path_buf() .file_name() - .ok_or("Current path does not include directory name".to_string())? + .context("Current path does not include directory name".to_string())? .to_str() - .ok_or("Current directory name is not a valid UTF-8 string".to_string())? + .context("Current directory name is not a valid UTF-8 string".to_string())? .to_string()) } @@ -167,10 +168,7 @@ pub(crate) fn post_create(path: &Path, vcs: &Vcs) -> Result<()> { // Only 1 error means that CWD isn't a cargo project. Err(cargo_metadata::Error::CargoMetadata { .. }) => None, Err(err) => { - return Err(Error::Other(anyhow::anyhow!( - "Couldn't retrieve cargo metadata: {:?}", - err - ))); + anyhow::bail!("Couldn't retrieve cargo metadata: {:?}", err) } } }; @@ -286,9 +284,9 @@ pub(crate) async fn connectivity_check() -> Result<()> { } } - Err(Error::Network( - "Error connecting to template repository. Try cloning the template manually or add `dioxus` to a `cargo new` project.".to_string(), - )) + bail!( + "Error connecting to template repository. Try cloning the template manually or add `dioxus` to a `cargo new` project." + ) } // todo: re-enable these tests with better parallelization diff --git a/packages/cli/src/cli/doctor.rs b/packages/cli/src/cli/doctor.rs new file mode 100644 index 000000000..948f2b500 --- /dev/null +++ b/packages/cli/src/cli/doctor.rs @@ -0,0 +1,212 @@ +use super::*; +use crate::{Result, Workspace}; +use anyhow::{bail, Context}; +use itertools::Itertools; + +/// Perform a system analysis to verify the system install is working correctly. +#[derive(Clone, Debug, Parser)] +pub(crate) struct Doctor {} + +impl Doctor { + pub async fn doctor(self) -> Result { + let Ok(workspace) = Workspace::current().await else { + bail!("dx doctor must be run within a cargo workspace!") + }; + + let rustc_version = &workspace.rustc_version; + let rustc_sysroot = &workspace.sysroot.to_string_lossy(); + let rustlib = workspace.sysroot.join("lib").join("rustlib"); + + // wasm-opt + let wasm_opt_location = crate::wasm_opt::installed_location(); + let wasm_opt_message = match wasm_opt_location.clone() { + Some(path) => path.to_string_lossy().to_string(), + None => "not installed".into(), + }; + + // wasm-bindgen + let wbg_version = workspace.wasm_bindgen_version(); + let wbg_version_msg = match wbg_version.clone() { + Some(msg) => msg, + None => "not required".to_string(), + }; + let wasm_bindgen_location = match wbg_version { + Some(vers) => match crate::wasm_bindgen::WasmBindgen::new(&vers).get_binary_path() { + Ok(path) => path.to_string_lossy().to_string(), + Err(err) => err.to_string().lines().join(""), + }, + None => "not required".to_string(), + }; + + // extensions + fn has_dioxus_ext(editor_dir: &str) -> anyhow::Result { + let home = dirs::home_dir().context("no home dir")?; + let exts = home.join(editor_dir).join("extensions"); + for dir in exts.read_dir()?.flatten() { + if dir + .file_name() + .to_string_lossy() + .contains("dioxuslabs.dioxus-") + { + return Ok(dir.path()); + } + } + + bail!("not found") + } + + // Editors + let vscode_ext = has_dioxus_ext(".vscode"); + let vscode_ext_msg = match vscode_ext.as_ref() { + Ok(path) => path.to_string_lossy().to_string(), + Err(_) => "not found".to_string(), + }; + let cursor_ext = has_dioxus_ext(".cursor"); + let cursor_ext_msg = match cursor_ext.as_ref() { + Ok(path) => path.to_string_lossy().to_string(), + Err(_) => "not found".to_string(), + }; + + // Tailwind + let mut tailwindcss = "not found".to_string(); + if let Ok(path) = crate::tailwind::TailwindCli::v3().get_binary_path() { + tailwindcss = path.display().to_string(); + } + if let Ok(path) = crate::tailwind::TailwindCli::v4().get_binary_path() { + tailwindcss = path.display().to_string(); + } + + let mut adb = "not found".to_string(); + let mut ndk = "not found".to_string(); + let mut sdk = "not found".to_string(); + let mut java_home = "not found".to_string(); + let mut emulator = "not found".to_string(); + if let Some(rf) = workspace.android_tools.as_deref() { + if rf.adb.exists() { + adb = rf.adb.display().to_string(); + } + if rf.ndk.exists() { + ndk = rf.ndk.display().to_string(); + } + if let Some(jh) = rf.java_home.as_ref() { + java_home = jh.display().to_string(); + } + if rf.sdk().exists() { + sdk = rf.sdk().display().to_string(); + } + if let Some(jh) = rf.java_home.as_ref() { + java_home = jh.display().to_string(); + } + if rf.emulator().exists() { + emulator = rf.emulator().display().to_string(); + } + }; + + let mut simulator_location = "not found".to_string(); + let mut xcode_install = "not found".to_string(); + if let Some(xcode) = workspace.xcode.as_ref() { + let sim_location = xcode.join("Applications").join("Simulator.app"); + if sim_location.exists() { + simulator_location = sim_location.display().to_string(); + } + if xcode.exists() { + xcode_install = xcode.display().to_string(); + } + } + + // toolchains + let mut has_wasm32_unknown_unknown = "❌"; + let mut has_aarch64_android_linux = "❌"; + let mut has_i686_linux_android = "❌"; + let mut has_armv7_linux_androideabi = "❌"; + let mut has_x86_64_android_linux = "❌"; + let mut has_x86_64_apple_ios = "❌"; + let mut has_aarch64_apple_ios = "❌"; + let mut has_aarch64_apple_ios_sim = "❌"; + let mut has_aarch64_apple_darwin = "❌"; + if rustlib.join("wasm32-unknown-unknown").exists() { + has_wasm32_unknown_unknown = "✅"; + } + if rustlib.join("aarch64-linux-android").exists() { + has_aarch64_android_linux = "✅"; + } + if rustlib.join("i686-linux-android").exists() { + has_i686_linux_android = "✅"; + } + if rustlib.join("armv7-linux-androideabi").exists() { + has_armv7_linux_androideabi = "✅"; + } + if rustlib.join("x86_64-linux-android").exists() { + has_x86_64_android_linux = "✅"; + } + if rustlib.join("x86_64-apple-ios").exists() { + has_x86_64_apple_ios = "✅"; + } + if rustlib.join("aarch64-apple-ios").exists() { + has_aarch64_apple_ios = "✅"; + } + if rustlib.join("aarch64-apple-ios-sim").exists() { + has_aarch64_apple_ios_sim = "✅"; + } + if rustlib.join("aarch64-apple-darwin").exists() { + has_aarch64_apple_darwin = "✅"; + } + + // Things to know + // - current rust version and rust-related things + // - installed toolchains + // - + use crate::styles::*; + println!( + r#"{LINK_STYLE}Setup{LINK_STYLE:#} + {GLOW_STYLE}Web{GLOW_STYLE:#}: wasm-bindgen, wasm-opt, and TailwindCSS are downloaded automatically + {GLOW_STYLE}iOS{GLOW_STYLE:#}: Install iOS SDK and developer tools and through XCode + {GLOW_STYLE}Android{GLOW_STYLE:#}: Install Android Studio, NDK, and then set ANDROID_HOME and ANDROID_NDK_HOME + {GLOW_STYLE}macOS{GLOW_STYLE:#}: all tools should be installed by default + {GLOW_STYLE}Windows{GLOW_STYLE:#}: install the webview2 binary + {GLOW_STYLE}Linux{GLOW_STYLE:#}: Install libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev + +{LINK_STYLE}Rust{LINK_STYLE:#} + Rustc version: {HINT_STYLE}{rustc_version}{HINT_STYLE:#} + Rustc sysroot: {HINT_STYLE}{rustc_sysroot}{HINT_STYLE:#} + +{LINK_STYLE}Devtools{LINK_STYLE:#} + VSCode Extension: {HINT_STYLE}{vscode_ext_msg}{HINT_STYLE:#} + Cursor Extension: {HINT_STYLE}{cursor_ext_msg}{HINT_STYLE:#} + TailwindCSS: {HINT_STYLE}{tailwindcss}{HINT_STYLE:#} + +{LINK_STYLE}Web{LINK_STYLE:#} + wasm-opt: {HINT_STYLE}{wasm_opt_message}{HINT_STYLE:#} + wasm-bindgen: {HINT_STYLE}{wasm_bindgen_location}{HINT_STYLE:#} + wasm-bindgen version: {HINT_STYLE}{wbg_version_msg}{HINT_STYLE:#} + +{LINK_STYLE}iOS{LINK_STYLE:#} + XCode: {HINT_STYLE}{xcode_install}{HINT_STYLE:#} + Simulator: {HINT_STYLE}{simulator_location}{HINT_STYLE:#} + +{LINK_STYLE}Android{LINK_STYLE:#} + sdk: {HINT_STYLE}{sdk}{HINT_STYLE:#} + ndk: {HINT_STYLE}{ndk}{HINT_STYLE:#} + adb: {HINT_STYLE}{adb}{HINT_STYLE:#} + emulator: {HINT_STYLE}{emulator}{HINT_STYLE:#} + java_home: {HINT_STYLE}{java_home}{HINT_STYLE:#} + +{LINK_STYLE}Toolchains{LINK_STYLE:#} + {HINT_STYLE}{has_wasm32_unknown_unknown}{HINT_STYLE:#} wasm32-unknown-unknown {HINT_STYLE}(web){HINT_STYLE:#} + {HINT_STYLE}{has_aarch64_android_linux}{HINT_STYLE:#} aarch64-android-linux {HINT_STYLE}(android){HINT_STYLE:#} + {HINT_STYLE}{has_i686_linux_android}{HINT_STYLE:#} i686-linux-android {HINT_STYLE}(android){HINT_STYLE:#} + {HINT_STYLE}{has_armv7_linux_androideabi}{HINT_STYLE:#} armv7-linux-androideabi {HINT_STYLE}(android){HINT_STYLE:#} + {HINT_STYLE}{has_x86_64_android_linux}{HINT_STYLE:#} x86_64-android-linux {HINT_STYLE}(android){HINT_STYLE:#} + {HINT_STYLE}{has_x86_64_apple_ios}{HINT_STYLE:#} x86_64-apple-ios {HINT_STYLE}(iOS){HINT_STYLE:#} + {HINT_STYLE}{has_aarch64_apple_ios}{HINT_STYLE:#} aarch64-apple-ios {HINT_STYLE}(iOS){HINT_STYLE:#} + {HINT_STYLE}{has_aarch64_apple_ios_sim}{HINT_STYLE:#} aarch64-apple-ios-sim {HINT_STYLE}(iOS){HINT_STYLE:#} + {HINT_STYLE}{has_aarch64_apple_darwin}{HINT_STYLE:#} aarch64-apple-darwin {HINT_STYLE}(iOS){HINT_STYLE:#} + +Get help: {LINK_STYLE}https://discord.gg/XgGxMSkvUM{LINK_STYLE:#} +More info: {LINK_STYLE}https://dioxuslabs.com/learn/0.6/{LINK_STYLE:#} +"# + ); + + Ok(StructuredOutput::Success) + } +} diff --git a/packages/cli/src/cli/mod.rs b/packages/cli/src/cli/mod.rs index 9a05342e0..0db95d7bd 100644 --- a/packages/cli/src/cli/mod.rs +++ b/packages/cli/src/cli/mod.rs @@ -3,9 +3,9 @@ pub(crate) mod build; pub(crate) mod build_assets; pub(crate) mod bundle; pub(crate) mod check; -pub(crate) mod clean; pub(crate) mod config; pub(crate) mod create; +pub(crate) mod doctor; pub(crate) mod init; pub(crate) mod link; pub(crate) mod platform_override; @@ -75,9 +75,9 @@ pub(crate) enum Commands { #[clap(name = "init")] Init(init::Init), - /// Clean output artifacts. - #[clap(name = "clean")] - Clean(clean::Clean), + /// Diagnose installed tools and system configuration. + #[clap(name = "doctor")] + Doctor(doctor::Doctor), /// Translate a source file into Dioxus code. #[clap(name = "translate")] @@ -121,7 +121,6 @@ impl Display for Commands { Commands::Serve(_) => write!(f, "serve"), Commands::New(_) => write!(f, "create"), Commands::Init(_) => write!(f, "init"), - Commands::Clean(_) => write!(f, "clean"), Commands::Config(_) => write!(f, "config"), Commands::Autoformat(_) => write!(f, "fmt"), Commands::Check(_) => write!(f, "check"), @@ -129,6 +128,7 @@ impl Display for Commands { Commands::Run(_) => write!(f, "run"), Commands::SelfUpdate(_) => write!(f, "self-update"), Commands::Tools(_) => write!(f, "tools"), + Commands::Doctor(_) => write!(f, "doctor"), } } } @@ -169,4 +169,5 @@ pub mod styles { pub(crate) const NOTE_STYLE: Style = AnsiColor::Green.on_default(); pub(crate) const LINK_STYLE: Style = AnsiColor::Blue.on_default(); pub(crate) const ERROR_STYLE: Style = AnsiColor::Red.on_default(); + pub(crate) const HINT_STYLE: Style = clap::builder::styling::Ansi256Color(244).on_default(); } diff --git a/packages/cli/src/cli/run.rs b/packages/cli/src/cli/run.rs index 6230a155e..eac2846bc 100644 --- a/packages/cli/src/cli/run.rs +++ b/packages/cli/src/cli/run.rs @@ -1,8 +1,9 @@ use super::*; use crate::{ serve::{AppServer, ServeUpdate, WebServer}, - BuilderUpdate, Error, Platform, Result, + BuilderUpdate, Platform, Result, }; +use anyhow::bail; use dioxus_dx_wire_format::BuildStage; /// Run the project with the given arguments @@ -29,9 +30,11 @@ impl RunArgs { self.args.hot_reload = Some(false); self.args.watch = Some(false); - let mut builder = AppServer::start(self.args).await?; + let mut builder = AppServer::new(self.args).await?; let mut devserver = WebServer::start(&builder)?; + builder.initialize(); + loop { let msg = tokio::select! { msg = builder.wait() => msg, @@ -109,15 +112,11 @@ impl RunArgs { } BuildStage::Failed => { tracing::error!("[{platform}] Build failed"); - return Err(Error::Cargo(format!( - "Build failed for platform: {platform}" - ))); + bail!("Build failed for platform: {platform}"); } BuildStage::Aborted => { tracing::error!("[{platform}] Build aborted"); - return Err(Error::Cargo(format!( - "Build aborted for platform: {platform}" - ))); + bail!("Build aborted for platform: {platform}"); } _ => {} }, @@ -125,7 +124,7 @@ impl RunArgs { print!("{}", message); } BuilderUpdate::BuildFailed { err } => { - tracing::error!("Build failed: {}", err); + tracing::error!("❌ Build failed: {}", err); return Err(err); } BuilderUpdate::StdoutReceived { msg } => { @@ -139,9 +138,7 @@ impl RunArgs { tracing::error!( "Application [{platform}] exited with error: {status}" ); - return Err(Error::Runtime(format!( - "Application [{platform}] exited with error: {status}" - ))); + bail!("Application [{platform}] exited with error: {status}"); } break; diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index ed392a141..a608089e8 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -181,7 +181,7 @@ impl ServeArgs { }) .unwrap_or_else(|| format!("dx serve panicked: {as_str}")); - Err(crate::error::Error::CapturedPanic(message)) + Err(anyhow::anyhow!(message)) } } } diff --git a/packages/cli/src/cli/translate.rs b/packages/cli/src/cli/translate.rs index c752396a6..bea96f929 100644 --- a/packages/cli/src/cli/translate.rs +++ b/packages/cli/src/cli/translate.rs @@ -1,5 +1,6 @@ use super::*; use crate::{Result, StructuredOutput}; +use anyhow::bail; use dioxus_rsx::{BodyNode, CallBody, TemplateBody}; /// Translate some source file into Dioxus code @@ -108,7 +109,7 @@ fn determine_input(file: Option, raw: Option) -> Result // Make sure not both are specified if file.is_some() && raw.is_some() { - return Err("Only one of --file or --raw should be specified.".into()); + bail!("Only one of --file or --raw should be specified."); } if let Some(raw) = raw { @@ -121,7 +122,7 @@ fn determine_input(file: Option, raw: Option) -> Result // If neither exist, we try to read from stdin if std::io::stdin().is_terminal() { - return Err(anyhow::anyhow!("No input file, source, or stdin to translate from.").into()); + bail!("No input file, source, or stdin to translate from."); } let mut buffer = String::new(); diff --git a/packages/cli/src/cli/update.rs b/packages/cli/src/cli/update.rs index 0985758d6..dee8676b2 100644 --- a/packages/cli/src/cli/update.rs +++ b/packages/cli/src/cli/update.rs @@ -1,6 +1,6 @@ use super::*; use crate::{Result, Workspace}; -use anyhow::Context; +use anyhow::{bail, Context}; use itertools::Itertools; use self_update::cargo_crate_version; @@ -93,7 +93,7 @@ impl SelfUpdate { } else if cfg!(target_arch = "aarch64") { "aarch64" } else { - return Err(Error::Unique("Unsupported architecture".to_string())); + bail!("Unsupported architecture"); }; let cur_os = if cfg!(target_os = "windows") { @@ -103,7 +103,7 @@ impl SelfUpdate { } else if cfg!(target_os = "macos") { "darwin" } else { - return Err(Error::Unique("Unsupported OS".to_string())); + bail!("Unsupported OS"); }; let zip_ext = "zip"; @@ -118,7 +118,7 @@ impl SelfUpdate { && a.name.contains(cur_arch) && a.name.ends_with(zip_ext) }) - .ok_or_else(|| Error::Unique("No suitable release found found".to_string()))?; + .context("No suitable asset found")?; let install_dir = Workspace::dioxus_home_dir().join("self-update"); std::fs::create_dir_all(&install_dir).context("Failed to create install directory")?; @@ -161,10 +161,7 @@ impl SelfUpdate { let executable = install_dir.join("dx"); if !executable.exists() { - return Err(Error::Unique(format!( - "Executable not found in {}", - install_dir.display() - ))); + bail!("Executable not found in {}", install_dir.display()); } tracing::info!( diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs index f70330471..9cee5b138 100644 --- a/packages/cli/src/error.rs +++ b/packages/cli/src/error.rs @@ -1,77 +1,33 @@ -use std::fmt::Debug; -use thiserror::Error as ThisError; - pub(crate) type Result = std::result::Result; +pub use anyhow::Error; +use itertools::Itertools; -#[derive(ThisError, Debug)] -pub(crate) enum Error { - /// Used when errors need to propagate but are too unique to be typed - #[error("{0}")] - Unique(String), +pub fn log_stacktrace(err: &anyhow::Error, padding: usize) -> String { + let mut trace = format!("{err}",); - #[error("I/O Error: {0}")] - IO(#[from] std::io::Error), - - #[error("Format Error: {0}")] - Format(#[from] std::fmt::Error), - - #[error("Format failed: {0}")] - Parse(String), - - #[error("Runtime Error: {0}")] - Runtime(String), - - #[error("Cargo Error: {0}")] - Cargo(String), - - #[error("Invalid proxy URL: {0}")] - InvalidProxy(#[from] hyper::http::uri::InvalidUri), - - #[error("Establishing proxy: {0}")] - ProxySetup(String), - - #[error("Bundling project: {0}")] - BundleFailed(#[from] tauri_bundler::Error), - - #[error("Performing hotpatch: {0}")] - PatchingFailed(#[from] crate::build::PatchError), - - #[error("Reading object file: {0}")] - ObjectReadFailed(#[from] object::Error), - - #[error("{0}")] - CapturedPanic(String), - - #[error("Rendering template error: {0}")] - TemplateParse(#[from] handlebars::RenderError), - - #[error("Network connectivity error: {0}")] - Network(String), - - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -impl From<&str> for Error { - fn from(s: &str) -> Self { - Error::Unique(s.to_string()) + for (idx, cause) in err.chain().enumerate().skip(1) { + trace.push_str(&format!( + "\n{}{IDX_STYLE}{idx}{IDX_STYLE:#}: {}", + " ".repeat(padding), + cause + .to_string() + .lines() + .enumerate() + .map(|(idx, line)| { + if idx == 0 { + line.to_string() + } else { + format!("{}{}", " ".repeat(padding + 3), line) + } + }) + .join("\n"), + IDX_STYLE = crate::styles::GLOW_STYLE, + )); } -} -impl From for Error { - fn from(s: String) -> Self { - Error::Unique(s) + if crate::VERBOSITY.get().unwrap().trace { + trace.push_str(&format!("\nBacktrace:\n{}", err.backtrace())); } -} -impl From for Error { - fn from(e: html_parser::Error) -> Self { - Self::Parse(e.to_string()) - } -} - -impl From for Error { - fn from(e: hyper::Error) -> Self { - Self::Runtime(e.to_string()) - } + trace } diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index 4ba8f2669..9184e86e5 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -56,13 +56,13 @@ async fn main() { Commands::Config(opts) => opts.config().await, Commands::Autoformat(opts) => opts.autoformat().await, Commands::Check(opts) => opts.check().await, - Commands::Clean(opts) => opts.clean().await, Commands::Build(opts) => opts.build().await, Commands::Serve(opts) => opts.serve().await, Commands::Bundle(opts) => opts.bundle().await, Commands::Run(opts) => opts.run().await, Commands::SelfUpdate(opts) => opts.self_update().await, Commands::Tools(BuildTools::BuildAssets(opts)) => opts.run().await, + Commands::Doctor(opts) => opts.doctor().await, }; // Provide a structured output for third party tools that can consume the output of the CLI @@ -71,17 +71,18 @@ async fn main() { tracing::debug!(json = ?output); } Err(err) => { - eprintln!( - "{ERROR_STYLE}failed:{ERROR_STYLE:#} {err}", - ERROR_STYLE = styles::ERROR_STYLE - ); - tracing::error!( json = ?StructuredOutput::Error { message: format!("{err:?}"), }, ); + eprintln!( + "{ERROR_STYLE}Failed{ERROR_STYLE:#}: {}", + crate::error::log_stacktrace(&err, 1), + ERROR_STYLE = crate::styles::ERROR_STYLE, + ); + std::process::exit(1); } }; diff --git a/packages/cli/src/serve/mod.rs b/packages/cli/src/serve/mod.rs index ecc22e4ca..735bc01b2 100644 --- a/packages/cli/src/serve/mod.rs +++ b/packages/cli/src/serve/mod.rs @@ -1,7 +1,6 @@ use crate::{ styles::{GLOW_STYLE, LINK_STYLE}, - AppBuilder, BuildId, BuildMode, BuilderUpdate, Error, Platform, Result, ServeArgs, - TraceController, + AppBuilder, BuildId, BuildMode, BuilderUpdate, Platform, Result, ServeArgs, TraceController, }; mod ansi_buffer; @@ -12,6 +11,7 @@ mod runner; mod server; mod update; +use anyhow::bail; use dioxus_dx_wire_format::BuildStage; pub(crate) use output::*; pub(crate) use runner::*; @@ -41,7 +41,7 @@ pub(crate) use update::*; pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> Result<()> { // Load the args into a plan, resolving all tooling, build dirs, arguments, decoding the multi-target, etc let exit_on_error = args.exit_on_error; - let mut builder = AppServer::start(args).await?; + let mut builder = AppServer::new(args).await?; let mut devserver = WebServer::start(&builder)?; let mut screen = Output::start(builder.interactive).await?; @@ -66,6 +66,8 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> } ); + builder.initialize(); + loop { // Draw the state of the server to the screen screen.render(&builder, &devserver); @@ -145,18 +147,14 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> stage: BuildStage::Failed, } => { if exit_on_error { - return Err(Error::Cargo(format!( - "Build failed for platform: {platform}" - ))); + bail!("Build failed for platform: {platform}"); } } BuilderUpdate::Progress { stage: BuildStage::Aborted, } => { if exit_on_error { - return Err(Error::Cargo(format!( - "Build aborted for platform: {platform}" - ))); + bail!("Build aborted for platform: {platform}"); } } BuilderUpdate::Progress { .. } => {} @@ -164,7 +162,12 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> screen.push_cargo_log(message); } BuilderUpdate::BuildFailed { err } => { - tracing::error!("Build failed: {}", err); + tracing::error!( + "{ERROR_STYLE}Build failed{ERROR_STYLE:#}: {}", + crate::error::log_stacktrace(&err, 15), + ERROR_STYLE = crate::styles::ERROR_STYLE, + ); + if exit_on_error { return Err(err); } @@ -184,7 +187,9 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> Err(err) => { tracing::error!("Failed to hot-patch app: {err}"); - if matches!(err, crate::Error::PatchingFailed(_)) { + if let Some(_patching) = + err.downcast_ref::() + { tracing::info!("Starting full rebuild: {err}"); builder.full_rebuild().await; devserver.send_reload_start().await; @@ -216,9 +221,7 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> } else { tracing::error!("Application [{platform}] exited with error: {status}"); if exit_on_error { - return Err(Error::Runtime(format!( - "Application [{platform}] exited with error: {status}" - ))); + bail!("Application [{platform}] exited with error: {status}"); } } } @@ -248,7 +251,10 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> } _ => { if let Err(err) = builder.open_all(&devserver, true).await { - tracing::error!("Failed to open app: {err}") + tracing::error!( + "Failed to open app: {}", + crate::error::log_stacktrace(&err, 15) + ) } } }, @@ -278,7 +284,7 @@ pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> _ = devserver.shutdown().await; match error { - Some(err) => return Err(anyhow::anyhow!("{}", err).into()), + Some(err) => return Err(err), None => return Ok(()), } } diff --git a/packages/cli/src/serve/output.rs b/packages/cli/src/serve/output.rs index f52002628..f105adc97 100644 --- a/packages/cli/src/serve/output.rs +++ b/packages/cli/src/serve/output.rs @@ -29,7 +29,7 @@ use tracing::Level; use super::AppServer; const TICK_RATE_MS: u64 = 100; -const VIEWPORT_MAX_WIDTH: u16 = 100; +const VIEWPORT_MAX_WIDTH: u16 = 90; const VIEWPORT_HEIGHT_SMALL: u16 = 5; const VIEWPORT_HEIGHT_BIG: u16 = 13; @@ -204,7 +204,7 @@ impl Output { Ok(Some(update)) => return update, Err(ee) => { return ServeUpdate::Exit { - error: Some(Box::new(ee)), + error: Some(anyhow::anyhow!(ee)), } } Ok(None) => {} @@ -675,7 +675,7 @@ impl Output { if client.build.platform == Platform::Web { "Serving at: ".gray() } else { - "ServerFns at: ".gray() + "Server at: ".gray() }, address, ])), diff --git a/packages/cli/src/serve/proxy.rs b/packages/cli/src/serve/proxy.rs index 028174fac..4765984a8 100644 --- a/packages/cli/src/serve/proxy.rs +++ b/packages/cli/src/serve/proxy.rs @@ -2,7 +2,7 @@ use crate::config::WebProxyConfig; use crate::TraceSrc; use crate::{Error, Result}; -use anyhow::{anyhow, Context}; +use anyhow::{bail, Context}; use axum::body::Body; use axum::http::request::Parts; use axum::{body::Body as MyBody, response::IntoResponse}; @@ -47,7 +47,7 @@ impl ProxyClient { self.inner .request(req) .await - .map_err(|err| crate::error::Error::Other(anyhow!(err))) + .context("Failed to send proxy request") } } @@ -64,11 +64,11 @@ pub(crate) fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result { - assert_eq!( - e, - "Proxy backend URL must have a non-empty path, e.g. http://localhost:8000/api instead of http://localhost:8000" - ); - } - e => panic!("Unexpected error type: {}", e), - } - } } diff --git a/packages/cli/src/serve/runner.rs b/packages/cli/src/serve/runner.rs index a00587d40..28bb938b2 100644 --- a/packages/cli/src/serve/runner.rs +++ b/packages/cli/src/serve/runner.rs @@ -1,10 +1,10 @@ use super::{AppBuilder, ServeUpdate, WebServer}; use crate::{ platform_override::CommandWithPlatformOverrides, BuildArtifacts, BuildId, BuildMode, - BuildTargets, BuilderUpdate, Error, HotpatchModuleCache, Platform, Result, ServeArgs, - TailwindCli, TraceSrc, Workspace, + BuildTargets, BuilderUpdate, HotpatchModuleCache, Platform, Result, ServeArgs, TailwindCli, + TraceSrc, Workspace, }; -use anyhow::Context; +use anyhow::{bail, Context}; use dioxus_core::internal::{ HotReloadTemplateWithLocation, HotReloadedTemplate, TemplateGlobalKey, }; @@ -92,7 +92,7 @@ pub(crate) struct CachedFile { impl AppServer { /// Create the AppRunner and then initialize the filemap with the crate directory. - pub(crate) async fn start(args: ServeArgs) -> Result { + pub(crate) async fn new(args: ServeArgs) -> Result { let workspace = Workspace::current().await?; // Resolve the simpler args @@ -115,7 +115,8 @@ impl AppServer { let open_browser = args .open - .unwrap_or_else(|| workspace.settings.always_open_browser.unwrap_or_default()); + .unwrap_or_else(|| workspace.settings.always_open_browser.unwrap_or(true)) + && interactive; let wsl_file_poll_interval = args .wsl_file_poll_interval @@ -161,15 +162,9 @@ impl AppServer { let watch_fs = args.watch.unwrap_or(true); let use_hotpatch_engine = args.hot_patch; - let build_mode = match use_hotpatch_engine { - true => BuildMode::Fat, - false => BuildMode::Base, - }; - let client = AppBuilder::start(&client, build_mode.clone())?; - let server = server - .map(|server| AppBuilder::start(&server, build_mode)) - .transpose()?; + let client = AppBuilder::new(&client)?; + let server = server.map(|server| AppBuilder::new(&server)).transpose()?; let tw_watcher = TailwindCli::serve( client.build.package_manifest_dir(), @@ -229,6 +224,18 @@ impl AppServer { Ok(runner) } + pub(crate) fn initialize(&mut self) { + let build_mode = match self.use_hotpatch_engine { + true => BuildMode::Fat, + false => BuildMode::Base, + }; + + self.client.start(build_mode.clone()); + if let Some(server) = self.server.as_mut() { + server.start(build_mode); + } + } + pub(crate) async fn rebuild_ssg(&mut self, devserver: &WebServer) { if self.client.stage != BuildStage::Success { return; @@ -355,9 +362,10 @@ impl AppServer { for path in files { // for various assets that might be linked in, we just try to hotreloading them forcefully // That is, unless they appear in an include! macro, in which case we need to a full rebuild.... - let Some(ext) = path.extension().and_then(|v| v.to_str()) else { - continue; - }; + let ext = path + .extension() + .and_then(|v| v.to_str()) + .unwrap_or_default(); // If it's an asset, we want to hotreload it // todo(jon): don't hardcode this here @@ -458,6 +466,21 @@ impl AppServer { } } } + + // If it's not a rust file, then it might be depended on via include! or similar + if ext != "rs" { + if let Some(artifacts) = self.client.artifacts.as_ref() { + if artifacts.depinfo.files.contains(path) { + needs_full_rebuild = true; + break; + } + } + } + } + + // If the client is in a failed state, any changes to rsx should trigger a rebuild/hotpatch + if self.client.stage == BuildStage::Failed && !templates.is_empty() { + needs_full_rebuild = true } // todo - we need to distinguish between hotpatchable rebuilds and true full rebuilds. @@ -685,7 +708,7 @@ impl AppServer { .hotpatch(res, cache) .await } - _ => return Err(Error::Runtime("Invalid build id".into())), + _ => bail!("Invalid build id"), }?; if id == BuildId::CLIENT { diff --git a/packages/cli/src/serve/server.rs b/packages/cli/src/serve/server.rs index f60eb99c3..308646c3c 100644 --- a/packages/cli/src/serve/server.rs +++ b/packages/cli/src/serve/server.rs @@ -2,7 +2,7 @@ use crate::{ config::WebHttpsConfig, serve::ServeUpdate, BuildId, BuildStage, BuilderUpdate, Platform, Result, TraceSrc, }; -use anyhow::Context; +use anyhow::{bail, Context}; use axum::{ body::Body, extract::{ @@ -635,8 +635,7 @@ async fn get_rustls(web_config: &WebHttpsConfig) -> Result<(String, String)> { { return Ok((cert, key)); } else { - // missing cert or key - return Err("https is enabled but cert or key path is missing".into()); + bail!("https is enabled but cert or key path is missing"); } } @@ -682,7 +681,7 @@ async fn get_rustls(web_config: &WebHttpsConfig) -> Result<(String, String)> { tracing::error!(dx_src = ?TraceSrc::Dev, "An error occurred while generating mkcert certificates: {}", e.to_string()) } }; - return Err("failed to generate mkcert certificates".into()); + bail!("failed to generate mkcert certificates"); } Ok(mut cmd) => { cmd.wait().await?; diff --git a/packages/cli/src/serve/update.rs b/packages/cli/src/serve/update.rs index dfb4b7405..fa8e98c4f 100644 --- a/packages/cli/src/serve/update.rs +++ b/packages/cli/src/serve/update.rs @@ -1,4 +1,4 @@ -use crate::{BuildId, BuilderUpdate, Platform, TraceMsg}; +use crate::{BuildId, BuilderUpdate, Error, Platform, TraceMsg}; use axum::extract::ws::Message as WsMessage; use std::path::PathBuf; @@ -44,6 +44,6 @@ pub(crate) enum ServeUpdate { }, Exit { - error: Option>, + error: Option, }, } diff --git a/packages/cli/src/settings.rs b/packages/cli/src/settings.rs index 13b020ba3..45542e697 100644 --- a/packages/cli/src/settings.rs +++ b/packages/cli/src/settings.rs @@ -1,4 +1,5 @@ use crate::{Result, TraceSrc}; +use anyhow::bail; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; use std::{fs, path::PathBuf, sync::Arc}; @@ -89,16 +90,14 @@ impl CliSettings { ?path, "failed to create directories for settings file" ); - return Err( - anyhow::anyhow!("failed to create directories for settings file: {e}").into(), - ); + bail!("failed to create directories for settings file: {e}"); } // Write the data. let result = fs::write(&path, data.clone()); if let Err(e) = result { error!(?data, ?path, "failed to save global cli settings"); - return Err(anyhow::anyhow!("failed to save global cli settings: {e}").into()); + bail!("failed to save global cli settings: {e}"); } Ok(()) diff --git a/packages/cli/src/tailwind.rs b/packages/cli/src/tailwind.rs index 649815c43..4877da5ab 100644 --- a/packages/cli/src/tailwind.rs +++ b/packages/cli/src/tailwind.rs @@ -141,6 +141,7 @@ impl TailwindCli { .arg("--output") .arg(output_path) .args(watch.then_some("--watch")) + .current_dir(manifest_dir) .kill_on_drop(true) .stdin(Stdio::piped()) .stdout(Stdio::null()) @@ -150,7 +151,7 @@ impl TailwindCli { Ok(proc) } - fn get_binary_path(&self) -> anyhow::Result { + pub fn get_binary_path(&self) -> anyhow::Result { if CliSettings::prefer_no_downloads() { which::which("tailwindcss").map_err(|_| anyhow!("Missing tailwindcss@{}", self.version)) } else { diff --git a/packages/cli/src/wasm_bindgen.rs b/packages/cli/src/wasm_bindgen.rs index 46b18d963..2bb30e722 100644 --- a/packages/cli/src/wasm_bindgen.rs +++ b/packages/cli/src/wasm_bindgen.rs @@ -4,7 +4,7 @@ use flate2::read::GzDecoder; use std::path::{Path, PathBuf}; use tar::Archive; use tempfile::TempDir; -use tokio::{fs, process::Command}; +use tokio::process::Command; pub(crate) struct WasmBindgen { version: String, @@ -100,7 +100,7 @@ impl WasmBindgen { /// Run the bindgen command with the current settings pub(crate) async fn run(&self) -> Result { - let binary = self.get_binary_path().await?; + let binary = self.get_binary_path()?; let mut args = Vec::new(); @@ -253,7 +253,7 @@ impl WasmBindgen { })?; // Get the final binary location. - let binary_path = self.get_binary_path().await?; + let binary_path = self.get_binary_path()?; // Download then extract wasm-bindgen-cli. let bytes = reqwest::get(url).await?.bytes().await?; @@ -300,11 +300,10 @@ impl WasmBindgen { .output() .await?; - fs::copy( + std::fs::copy( tempdir.path().join(self.downloaded_bin_name()), - self.get_binary_path().await?, - ) - .await?; + self.get_binary_path()?, + )?; Ok(()) } @@ -336,11 +335,10 @@ impl WasmBindgen { tracing::info!("Copying into path: {}", tempdir.path().display()); // copy the wasm-bindgen out of the tempdir to the final location - fs::copy( + std::fs::copy( tempdir.path().join("bin").join(self.downloaded_bin_name()), - self.get_binary_path().await?, + self.get_binary_path()?, ) - .await .context("failed to copy wasm-bindgen binary")?; Ok(()) @@ -352,7 +350,7 @@ impl WasmBindgen { self.version ); - let binary = self.get_binary_path().await?; + let binary = self.get_binary_path()?; let output = Command::new(binary) .args(["--version"]) .output() @@ -381,7 +379,7 @@ impl WasmBindgen { ); let binary_name = self.installed_bin_name(); - let path = self.install_dir().await?.join(binary_name); + let path = self.install_dir()?.join(binary_name); if !path.exists() { self.install().await?; @@ -390,20 +388,20 @@ impl WasmBindgen { Ok(()) } - async fn get_binary_path(&self) -> anyhow::Result { + pub fn get_binary_path(&self) -> anyhow::Result { if CliSettings::prefer_no_downloads() { which::which("wasm-bindgen") .map_err(|_| anyhow!("Missing wasm-bindgen-cli@{}", self.version)) } else { let installed_name = self.installed_bin_name(); - let install_dir = self.install_dir().await?; + let install_dir = self.install_dir()?; Ok(install_dir.join(installed_name)) } } - async fn install_dir(&self) -> anyhow::Result { + fn install_dir(&self) -> anyhow::Result { let bindgen_dir = Workspace::dioxus_home_dir().join("wasm-bindgen/"); - fs::create_dir_all(&bindgen_dir).await?; + std::fs::create_dir_all(&bindgen_dir)?; Ok(bindgen_dir) } @@ -501,7 +499,7 @@ mod test { /// Helper to test that the installed binary actually exists. async fn verify_installation(binary: &WasmBindgen) { - let path = binary.install_dir().await.unwrap(); + let path = binary.install_dir().unwrap(); let name = binary.installed_bin_name(); let binary_path = path.join(name); assert!( @@ -513,9 +511,9 @@ mod test { /// Delete the installed binary. The temp folder should be automatically deleted. async fn reset_test() { let binary = WasmBindgen::new(VERSION); - let path = binary.install_dir().await.unwrap(); + let path = binary.install_dir().unwrap(); let name = binary.installed_bin_name(); let binary_path = path.join(name); - let _ = tokio::fs::remove_file(binary_path).await; + let _ = std::fs::remove_file(binary_path); } } diff --git a/packages/cli/src/wasm_opt.rs b/packages/cli/src/wasm_opt.rs index d3f4d45db..e35b8d9f6 100644 --- a/packages/cli/src/wasm_opt.rs +++ b/packages/cli/src/wasm_opt.rs @@ -1,6 +1,6 @@ use crate::config::WasmOptLevel; use crate::{CliSettings, Result, WasmOptConfig, Workspace}; -use anyhow::{anyhow, Context}; +use anyhow::{anyhow, bail, Context}; use flate2::read::GzDecoder; use std::path::{Path, PathBuf}; use tar::Archive; @@ -16,8 +16,11 @@ pub async fn write_wasm(bytes: &[u8], output_path: &Path, cfg: &WasmOptConfig) - pub async fn optimize(input_path: &Path, output_path: &Path, cfg: &WasmOptConfig) -> Result<()> { let wasm_opt = WasmOpt::new(input_path, output_path, cfg) .await - .inspect_err(|err| tracing::error!("Failed to create wasm-opt instance: {}", err))?; - wasm_opt.optimize().await?; + .context("Failed to create wasm-opt instance")?; + wasm_opt + .optimize() + .await + .context("Failed to run wasm-opt")?; Ok(()) } @@ -170,9 +173,7 @@ async fn find_latest_wasm_opt_download_url() -> anyhow::Result { } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) { "arm64-macos" } else { - return Err(anyhow::anyhow!( - "Unknown platform for wasm-opt installation. Please install wasm-opt manually from https://github.com/WebAssembly/binaryen/releases and add it to your PATH." - )); + bail!("Unknown platform for wasm-opt installation. Please install wasm-opt manually from https://github.com/WebAssembly/binaryen/releases and add it to your PATH."); }; // Find the first asset with a name that contains the platform string @@ -184,12 +185,10 @@ async fn find_latest_wasm_opt_download_url() -> anyhow::Result { .and_then(|name| name.as_str()) .is_some_and(|name| name.contains(platform) && !name.ends_with("sha256")) }) - .ok_or_else(|| { - anyhow::anyhow!( - "No suitable wasm-opt binary found for platform: {}. Please install wasm-opt manually from https://github.com/WebAssembly/binaryen/releases and add it to your PATH.", - platform - ) - })?; + .with_context(|| anyhow!( + "No suitable wasm-opt binary found for platform: {}. Please install wasm-opt manually from https://github.com/WebAssembly/binaryen/releases and add it to your PATH.", + platform + ))?; // Extract the download URL from the asset let download_url = asset @@ -201,7 +200,7 @@ async fn find_latest_wasm_opt_download_url() -> anyhow::Result { } /// Get the path to the wasm-opt binary, downloading it if necessary -async fn get_binary_path() -> anyhow::Result { +pub async fn get_binary_path() -> anyhow::Result { let install_dir = install_dir(); let install_path = installed_bin_path(&install_dir); @@ -224,6 +223,25 @@ async fn get_binary_path() -> anyhow::Result { Ok(install_path) } +pub fn installed_location() -> Option { + let install_dir = install_dir(); + let install_path = installed_bin_path(&install_dir); + + if install_path.exists() { + return Some(install_path); + } + + if CliSettings::prefer_no_downloads() { + if let Ok(existing) = which::which("wasm-opt") { + return Some(existing); + } else { + return None; + } + } + + None +} + fn install_dir() -> PathBuf { Workspace::dioxus_home_dir().join("binaryen") } diff --git a/packages/cli/src/workspace.rs b/packages/cli/src/workspace.rs index a334a8fc0..31aa7a354 100644 --- a/packages/cli/src/workspace.rs +++ b/packages/cli/src/workspace.rs @@ -2,7 +2,7 @@ use crate::styles::GLOW_STYLE; use crate::CliSettings; use crate::Result; use crate::{config::DioxusConfig, AndroidTools}; -use anyhow::Context; +use anyhow::{bail, Context}; use ignore::gitignore::Gitignore; use krates::{semver::Version, KrateDetails, LockOptions}; use krates::{Cmd, Krates, NodeId}; @@ -21,6 +21,7 @@ pub struct Workspace { pub(crate) ignore: Gitignore, pub(crate) cargo_toml: cargo_toml::Manifest, pub(crate) android_tools: Option>, + pub(crate) xcode: Option, } impl Workspace { @@ -48,9 +49,7 @@ impl Workspace { let mut builder = krates::Builder::new(); builder.workspace(true); - let res = builder - .build(cmd, |_| {}) - .context("Failed to run cargo metadata"); + let res = builder.build(cmd, |_| {})?; if !lock_options.offline { if let Ok(res) = std::env::var("SIMULATE_SLOW_NETWORK") { @@ -58,7 +57,7 @@ impl Workspace { } } - res + Ok(res) as Result }); let spin_future = async move { @@ -76,10 +75,14 @@ impl Workspace { }; let krates = tokio::select! { - f = krates_future => f.context("failed to run cargo metadata")??, - _ = spin_future => return Err(crate::Error::Network( - "cargo metadata took too long to respond, try again with --offline".to_string(), - )), + f = krates_future => { + let res = f?; + if let Err(krates::Error::Metadata(e)) = res { + bail!("{e}"); + } + res? + }, + _ = spin_future => bail!("cargo metadata took too long to respond, try again with --offline"), }; let settings = CliSettings::global_or_default(); @@ -107,6 +110,13 @@ impl Workspace { let android_tools = crate::build::get_android_tools(); + let xcode = Command::new("xcode-select") + .arg("-p") + .output() + .await + .ok() + .map(|s| String::from_utf8_lossy(&s.stdout).trim().to_string().into()); + let workspace = Arc::new(Self { krates, settings, @@ -116,6 +126,7 @@ impl Workspace { ignore, cargo_toml, android_tools, + xcode, }); tracing::debug!( @@ -141,10 +152,10 @@ impl Workspace { } pub fn android_tools(&self) -> Result> { - Ok(self + self .android_tools .clone() - .context("Android not installed properly. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation.")?) + .context("Android not installed properly. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation.") } pub fn is_release_profile(&self, profile: &str) -> bool { @@ -195,7 +206,10 @@ impl Workspace { let max = dioxus_versions.iter().max().unwrap(); // If the minimum dioxus version is greater than the current cli version, warn the user - if min > &dx_semver || max < &dx_semver { + if min > &dx_semver + || max < &dx_semver + || dioxus_versions.iter().any(|f| f.pre != dx_semver.pre) + { tracing::error!( r#"🚫dx and dioxus versions are incompatible! • dx version: {dx_semver} @@ -249,6 +263,14 @@ impl Workspace { self.gcc_ld_dir().join("wasm-ld") } + /// Return the version of the wasm-bindgen crate if it exists + pub fn wasm_bindgen_version(&self) -> Option { + self.krates + .krates_by_name("wasm-bindgen") + .next() + .map(|krate| krate.krate.version.to_string()) + } + // wasm-ld: ./rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/wasm-ld // rust-lld: ./rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/rust-lld fn gcc_ld_dir(&self) -> PathBuf { @@ -377,7 +399,7 @@ impl Workspace { toml::from_str::(&std::fs::read_to_string(&dioxus_conf_file)?) .map_err(|err| { - anyhow::anyhow!("Failed to parse Dioxus.toml at {dioxus_conf_file:?}: {err}").into() + anyhow::anyhow!("Failed to parse Dioxus.toml at {dioxus_conf_file:?}: {err}") }) .map(Some) } @@ -459,24 +481,18 @@ impl Workspace { } None }) - .ok_or_else(|| { - crate::Error::Cargo("Failed to find directory containing Cargo.toml".to_string()) - }) + .context("Failed to find directory containing Cargo.toml") } /// Returns the properly canonicalized path to the dx executable, used for linking and wrapping rustc pub(crate) fn path_to_dx() -> Result { - Ok( - dunce::canonicalize(std::env::current_exe().context("Failed to find dx")?) - .context("Failed to find dx")?, - ) + dunce::canonicalize(std::env::current_exe().context("Failed to find dx")?) + .context("Failed to find dx") } /// Returns the path to the dioxus home directory, used to install tools and other things pub(crate) fn dioxus_home_dir() -> PathBuf { - dirs::data_local_dir() - .map(|f| f.join("dioxus/")) - .unwrap_or_else(|| dirs::home_dir().unwrap().join(".dioxus")) + dirs::home_dir().unwrap().join(".dioxus") } } diff --git a/packages/depinfo/src/lib.rs b/packages/depinfo/src/lib.rs index a14e6d28a..c5ef6e9fe 100644 --- a/packages/depinfo/src/lib.rs +++ b/packages/depinfo/src/lib.rs @@ -3,7 +3,7 @@ //! Used by the hot-reloading engine and other libraries to provide higher quality dependency analysis //! for the user's project. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[non_exhaustive] #[derive(Debug, thiserror::Error)] @@ -18,7 +18,7 @@ pub enum DepInfoParseError { } #[non_exhaustive] -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct RustcDepInfo { /// The list of files that the main target in the dep-info file depends on. pub files: Vec, @@ -42,6 +42,12 @@ impl std::str::FromStr for RustcDepInfo { } impl RustcDepInfo { + pub fn from_file(path: &Path) -> Result { + let contents = + std::fs::read_to_string(path).map_err(|_| DepInfoParseError::MalformedInput)?; + RustcDepInfo::new(&contents) + } + /// Parse the `.d` dep-info file generated by rustc. pub fn new(contents: &str) -> Result { let mut ret = RustcDepInfo::default(); diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 1f4c881a3..27401f5c8 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -79,6 +79,11 @@ ndk = { version = "0.9.0" } ndk-sys = { version = "0.6.0" } ndk-context = { version = "0.1.1" } +# The `openssl` dependency generally does not know how to cross-compile for Android. To make the lives +# of our users easier, we automatically enable the `openssl` feature when building for Android. +# Feature are additive, so users will need to "subtract" this feature if they do not want it. +openssl = { version = "0.10", features = ["vendored"], optional = true } + # use native tls on other platforms [target.'cfg(not(target_os = "android"))'.dependencies] tungstenite = { workspace = true, features = ["native-tls"] } @@ -92,11 +97,12 @@ objc = "0.2.7" lazy-js-bundle = { workspace = true } [features] -default = ["tokio_runtime", "transparent", "devtools"] +default = ["tokio_runtime", "transparent", "devtools", "openssl-vendored-android"] tokio_runtime = ["dep:tokio"] fullscreen = ["wry/fullscreen"] devtools = ["wry/devtools", "dep:dioxus-devtools", "dioxus-signals"] transparent = ["wry/transparent"] +openssl-vendored-android = ["openssl"] gnu = [] [package.metadata.docs.rs] diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 02dc2f0e3..d4051bf4b 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -18,6 +18,7 @@ mod file_upload; mod hooks; mod ipc; mod menubar; +mod mobile; mod protocol; mod query; mod shortcut; diff --git a/packages/desktop/src/mobile.rs b/packages/desktop/src/mobile.rs new file mode 100644 index 000000000..a8c219620 --- /dev/null +++ b/packages/desktop/src/mobile.rs @@ -0,0 +1,65 @@ +/// 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() { + use crate::Config; + use dioxus_core::{Element, VirtualDom}; + use std::any::Any; + + tao::android_binding!(dev_dioxus, main, WryActivity, wry::android_setup, root, tao); + wry::android_binding!(dev_dioxus, main, wry); + + #[cfg(target_os = "android")] + fn root() { + fn stop_unwind T, T>(f: F) -> T { + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) { + Ok(t) => t, + Err(err) => { + eprintln!("attempt to unwind out of `rust` with err: {:?}", err); + std::process::abort() + } + } + } + + stop_unwind(|| 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 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. + let env_file = dioxus_cli_config::android_session_cache_dir().join(".env"); + if let Ok(env_file) = std::fs::read_to_string(&env_file) { + for line in env_file.lines() { + if let Some((key, value)) = line.trim().split_once('=') { + std::env::set_var(key, value); + } + } + } + } + + let main_fn: extern "C" fn() = std::mem::transmute(main_fn_ptr); + main_fn(); + }); + } +} diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index cf1992a5f..84c290222 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -22,7 +22,6 @@ dioxus-hooks = { workspace = true, optional = true } dioxus-signals = { workspace = true, optional = true } dioxus-router = { workspace = true, optional = true } dioxus-web = { workspace = true, default-features = false, optional = true } -dioxus-mobile = { workspace = true, optional = true } dioxus-desktop = { workspace = true, default-features = true, optional = true } dioxus-fullstack = { workspace = true, default-features = true, optional = true } dioxus-liveview = { workspace = true, optional = true } @@ -80,7 +79,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", "dep:dioxus-desktop", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobile"] +mobile = ["dep:dioxus-desktop", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobile"] web = [ "dep:dioxus-web", "dioxus-fullstack?/web", diff --git a/packages/dioxus/src/launch.rs b/packages/dioxus/src/launch.rs index a94cbd43b..efb8e2e97 100644 --- a/packages/dioxus/src/launch.rs +++ b/packages/dioxus/src/launch.rs @@ -332,7 +332,7 @@ impl LaunchBuilder { #[cfg(feature = "mobile")] if matches!(platform, KnownPlatform::Mobile) { - return dioxus_mobile::launch_bindings::launch(app, contexts, configs); + return dioxus_desktop::launch::launch(app, contexts, configs); } #[cfg(feature = "desktop")] diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index f14b72b81..b485c9a5d 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -100,7 +100,7 @@ pub use dioxus_desktop as desktop; #[cfg(feature = "mobile")] #[cfg_attr(docsrs, doc(cfg(feature = "mobile")))] -pub use dioxus_mobile as mobile; +pub use dioxus_desktop as mobile; #[cfg(feature = "liveview")] #[cfg_attr(docsrs, doc(cfg(feature = "liveview")))] diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 9897d0612..d8ff2fa83 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -36,9 +36,6 @@ dioxus-interpreter-js = { workspace = true, optional = true } # Desktop Integration dioxus-desktop = { workspace = true, optional = true } -# Mobile Integration -dioxus-mobile = { workspace = true, optional = true } - tracing = { workspace = true } tracing-futures = { workspace = true, optional = true } tokio-util = { workspace = true, features = ["rt"], optional = true } diff --git a/packages/manganis/manganis-core/src/asset.rs b/packages/manganis/manganis-core/src/asset.rs index e467febea..bcef65511 100644 --- a/packages/manganis/manganis-core/src/asset.rs +++ b/packages/manganis/manganis-core/src/asset.rs @@ -120,7 +120,7 @@ impl Asset { #[doc(hidden)] /// This should only be called from the macro /// Create a new asset from the bundled form of the asset and the link section - pub const fn new(bundled: fn() -> &'static [u8]) -> Self { + pub const fn new(bundled: extern "Rust" fn() -> &'static [u8]) -> Self { Self { bundled } } diff --git a/packages/manganis/manganis-macro/src/asset.rs b/packages/manganis/manganis-macro/src/asset.rs index 09b183415..83c217cd5 100644 --- a/packages/manganis/manganis-macro/src/asset.rs +++ b/packages/manganis/manganis-macro/src/asset.rs @@ -120,9 +120,9 @@ impl ToTokens for AssetParser { static __REFERENCE_TO_LINK_SECTION: &'static [u8] = &__LINK_SECTION; - manganis::Asset::new( - || __REFERENCE_TO_LINK_SECTION - ) + + + manganis::Asset::new(|| unsafe { std::ptr::read_volatile(&__REFERENCE_TO_LINK_SECTION) }) } }) } diff --git a/packages/mobile/.cargo/config.toml b/packages/mobile/.cargo/config.toml deleted file mode 100644 index 2a9579151..000000000 --- a/packages/mobile/.cargo/config.toml +++ /dev/null @@ -1,5 +0,0 @@ -[build] -target = [ - "aarch64-apple-ios", - "aarch64-linux-android" -] diff --git a/packages/mobile/.vscode/settings.json b/packages/mobile/.vscode/settings.json deleted file mode 100644 index 917c00cff..000000000 --- a/packages/mobile/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "rust-analyzer.inlayHints.enable": true, - "rust-analyzer.checkOnSave.allTargets": true, -} diff --git a/packages/mobile/Cargo.toml b/packages/mobile/Cargo.toml deleted file mode 100644 index b89182fa2..000000000 --- a/packages/mobile/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "dioxus-mobile" -version = { workspace = true } -authors = ["Jonathan Kelley"] -edition = "2021" -description = "Mobile-compatible renderer for Dioxus" -repository = "https://github.com/DioxusLabs/dioxus/" -homepage = "https://dioxuslabs.com/learn/0.6/getting_started" -keywords = ["dom", "ui", "gui", "react"] -license = "MIT OR Apache-2.0" -rust-version = "1.71.1" - -[dependencies] -dioxus-desktop = { workspace = true } -dioxus-lib = { workspace = true } -dioxus-cli-config = { workspace = true } -libc = "0.2.170" - -[target.'cfg(target_os = "android")'.dependencies] -jni = "0.21.1" - -[features] -default = ["jnibindings", "tokio_runtime", "devtools"] -jnibindings = [] -tokio_runtime = ["dioxus-desktop/tokio_runtime"] -fullscreen = ["dioxus-desktop/fullscreen"] -transparent = ["dioxus-desktop/transparent"] -devtools = ["dioxus-desktop/devtools"] -gnu = ["dioxus-desktop/gnu"] - - -[lib] -doctest = false -# tests suspended until package ready -test = false - -[package.metadata.docs.rs] -cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] diff --git a/packages/mobile/Makefile.toml b/packages/mobile/Makefile.toml deleted file mode 100644 index 90f08a170..000000000 --- a/packages/mobile/Makefile.toml +++ /dev/null @@ -1,7 +0,0 @@ -[tasks.test] -command = "cargo" -args = [ - "test", - "--no-run", -] - diff --git a/packages/mobile/README.md b/packages/mobile/README.md deleted file mode 100644 index ba7fdeb57..000000000 --- a/packages/mobile/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Dioxus Mobile - -[![Crates.io][crates-badge]][crates-url] -[![MIT licensed][mit-badge]][mit-url] -[![Build Status][actions-badge]][actions-url] -[![Discord chat][discord-badge]][discord-url] - -[crates-badge]: https://img.shields.io/crates/v/dioxus-mobile.svg -[crates-url]: https://crates.io/crates/dioxus-mobile -[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg -[mit-url]: https://github.com/dioxuslabs/dioxus/blob/main/LICENSE-MIT -[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg -[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster -[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square -[discord-url]: https://discord.gg/XgGxMSkvUM - -[Website](https://dioxuslabs.com) | -[Guides](https://dioxuslabs.com/learn/0.6/) | -[API Docs](https://docs.rs/dioxus-mobile/latest/dioxus_mobile) | -[Chat](https://discord.gg/XgGxMSkvUM) - -## Overview - -`dioxus-mobile` is a re-export of `dioxus-desktop` with some minor tweaks and documentation changes. As this crate evolves, it will provide some more unique features to mobile, but for now, it's very similar to the desktop crate. - -Dioxus Mobile supports both iOS and Android. However, Android support is still quite experimental and requires a lot of configuration. A good area to contribute here would be to improve the CLI tool to include bundling and mobile configuration. - -## Getting Set up - -Getting set up with mobile can but quite challenging. The tooling here isn't great (yet) and might take some hacking around to get things working. macOS M1 is broadly unexplored and might not work for you. - -You can read [our guide](https://dioxuslabs.com/learn/0.6/getting_started) on mobile development with Dioxus to get started. - -## Contributing - -- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues). -- Join the discord and ask questions! - -## License - -This project is licensed under the [MIT license]. - -[mit license]: https://github.com/dioxuslabs/dioxus/blob/main/LICENSE-MIT - -Unless you explicitly state otherwise, any contribution intentionally submitted -for inclusion in Dioxus by you shall be licensed as MIT without any additional -terms or conditions. diff --git a/packages/mobile/src/lib.rs b/packages/mobile/src/lib.rs deleted file mode 100644 index 9ce7ba52d..000000000 --- a/packages/mobile/src/lib.rs +++ /dev/null @@ -1,105 +0,0 @@ -#![doc = include_str!("../README.md")] -#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] -#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] - -pub use dioxus_desktop::*; -// intentionally shadow the launch module as it does not work on mobile. -pub mod launch {} - -use dioxus_lib::prelude::*; -use std::any::Any; - -pub mod launch_bindings { - - use super::*; - pub fn launch( - root: fn() -> Element, - contexts: Vec Box + Send + Sync>>, - platform_config: Vec>, - ) { - 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) { - launch_cfg(root, vec![], vec![]); -} - -pub fn launch_cfg( - root: fn() -> Element, - contexts: Vec Box + Send + Sync>>, - platform_config: Vec>, -) { - dioxus_desktop::launch::launch(root, contexts, platform_config); -} - -#[doc(hidden)] -pub fn root() { - fn stop_unwind T, T>(f: F) -> T { - match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) { - Ok(t) => t, - Err(err) => { - eprintln!("attempt to unwind out of `rust` with err: {:?}", err); - std::process::abort() - } - } - } - - stop_unwind(|| 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(); - }); -} - -/// 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); -} - -/// 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. -fn load_env_file_from_session_cache() { - let env_file = dioxus_cli_config::android_session_cache_dir().join(".env"); - if let Ok(env_file) = std::fs::read_to_string(&env_file) { - for line in env_file.lines() { - if let Some((key, value)) = line.trim().split_once('=') { - std::env::set_var(key, value); - } - } - } -} diff --git a/packages/playwright-tests/nested-suspense/Cargo.toml b/packages/playwright-tests/nested-suspense/Cargo.toml index 70fc6be81..67f6ca83d 100644 --- a/packages/playwright-tests/nested-suspense/Cargo.toml +++ b/packages/playwright-tests/nested-suspense/Cargo.toml @@ -13,7 +13,7 @@ tokio = { workspace = true, features = ["full"], optional = true } [features] default = [] -server = ["dioxus/server", "tokio"] +server = ["dioxus/server", "dep:tokio"] web = ["dioxus/web"] # We need a separate bin for the SSG build to avoid conflicting server caches diff --git a/translations/pt-br/README.md b/translations/pt-br/README.md index 3139e7453..0658cf512 100644 --- a/translations/pt-br/README.md +++ b/translations/pt-br/README.md @@ -95,9 +95,9 @@ Se você conhece React, então você já conhece o Dioxus. ## Projetos de Exemplo: -| Navegador de Arquivos (Desktop) | WiFi Scanner (Desktop) | TodoMVC (Todas as Plataformas) | E-commerce com Tailwind (SSR/LiveView) | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [![Explorador de Arquivos](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer/assets/image.png)](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [![Wifi Scanner Demo](https://github.com/DioxusLabs/example-projects/raw/master/wifi-scanner/demo_small.png)](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [![Exemplo de E-commerce](https://github.com/DioxusLabs/example-projects/raw/master/ecommerce-site/demo.png)](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) | +| Navegador de Arquivos (Desktop) | WiFi Scanner (Desktop) | TodoMVC (Todas as Plataformas) | E-commerce com Tailwind (SSR/LiveView) | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [![Explorador de Arquivos](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer/assets/image.png)](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [![Wifi Scanner Demo](/example-projects/bluetooth-scanner/demo_small.png)](/example-projects/bluetooth-scanner/) | [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [![Exemplo de E-commerce](https://github.com/DioxusLabs/example-projects/raw/master/ecommerce-site/demo.png)](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) | Veja a página [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) para uma lista curada do conteúdo dentro do ecossistema do Dioxus.