rip out tauri bundle (v2), swc -> esbuild, blacksmith -> warpbuild, FAST CI 🚀 (#5328)

- inline mac, windows, linux bundling in the cli
- remove tauri bundle from our dep tree
- trim down the rpm crate for fewer dependencies in tree
- migrate from blacksmith to warpbuild
- move off swc and instead use esbuild via tool
- use dev drives in windows CI
- use fork of nix action for faster caching
- test bundling in CI
- fix asset handling in linux 
- add default APPDATA location for webview2 installs for dioxus apps on windows
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2026-03-20 12:27:13 -07:00
1 parent be7296f4f0
commit b2d01e6779
83 files changed
+7756 -5403

No files matched your search

+377 -171
View File
@@ -1,34 +1,19 @@
# Whenever an open PR is updated, the workflow will be triggered # Whenever an open PR is updated, the workflow will be triggered
# #
# This can get expensive, so we do a lot of caching and checks to prevent unnecessary runs # This can get expensive, so we do a lot of caching and checks to prevent unnecessary runs
# Language linting (typos, link-check) runs on every push; Rust jobs are gated behind path checks
name: Rust CI name: CI
on: on:
push: push:
branches: branches:
- main - main
paths:
- packages/**
- examples/**
- docs/guide/**
- src/**
- .github/**
- lib.rs
- Cargo.toml
- Makefile.toml
pull_request: pull_request:
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
branches: branches:
- main - main
paths:
- packages/**
- examples/**
- src/**
- .github/**
- lib.rs
- Cargo.toml
# workflow_dispatch: # workflow_dispatch:
concurrency: concurrency:
@@ -38,110 +23,113 @@ concurrency:
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0 # todo(jon): cargo-cache wipes incremental artifacts, but we eventually want to cache them CARGO_INCREMENTAL: 0 # todo(jon): cargo-cache wipes incremental artifacts, but we eventually want to cache them
# CARGO_PROFILE_DEV_PACKAGE_STAR_STRIP: debuginfo # strip debug info from deps to shrink cache
RUST_BACKTRACE: 1 RUST_BACKTRACE: 1
rust_nightly: nightly-2025-10-05 rust_nightly: nightly-2025-10-05
jobs: jobs:
check-msrv: # ---------------------------------------------------------------------------
if: github.event.pull_request.draft == false # Path gate — determines whether Rust jobs should run
name: Check MSRV # ---------------------------------------------------------------------------
runs-on: blacksmith-4vcpu-ubuntu-2404 check-paths:
runs-on: warp-ubuntu-latest-x64-4x
outputs:
rust: ${{ steps.filter.outputs.rust }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.88.0 - uses: dorny/paths-filter@v3
- uses: Swatinem/rust-cache@v2 id: filter
with: with:
cache-all-crates: "true" filters: |
# https://github.com/foresterre/cargo-msrv/blob/4345edfe3f4fc91cc8ae6c7d6804c0748fae92ae/.github/workflows/msrv.yml rust:
- name: install_cargo_msrv - 'packages/**'
run: cargo install cargo-msrv --all-features --version 0.16.3 --locked - 'examples/**'
- name: version_of_cargo_msrv - 'docs/guide/**'
run: cargo msrv --version - 'src/**'
- name: run_cargo_msrv - '.github/**'
run: cargo msrv --output-format json verify -- cargo check - 'lib.rs'
- name: run_cargo_msrv_on_verify_failure - 'Cargo.toml'
if: ${{ failure() }} - 'Makefile.toml'
run: cargo msrv --output-format json -- cargo check
test: # ---------------------------------------------------------------------------
# Language linting — runs on every push
# ---------------------------------------------------------------------------
typos:
if: github.event.pull_request.draft == false if: github.event.pull_request.draft == false
name: Test Suite name: "Lint | Typos"
runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v5
- name: Check for typos
uses: crate-ci/typos@master
link-check:
if: github.event.pull_request.draft == false
name: "Lint | Links"
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v5
- name: Restore lychee cache
uses: actions/cache@v5
with:
path: .lycheecache
key: cache-lychee-${{ github.sha }}
restore-keys: cache-lychee-
- name: Run lychee
uses: lycheeverse/lychee-action@v2
with:
args: --config ./lychee.toml './**/*.md'
fail: true
# ---------------------------------------------------------------------------
# Rust jobs — only run when relevant paths change
# ---------------------------------------------------------------------------
lint:
needs: check-paths
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
name: "Lint | Clippy, fmt, schema, msrv"
runs-on: warp-ubuntu-latest-x64-4x
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Free Disk Space
uses: ./.github/actions/free-disk-space
- uses: awalsh128/cache-apt-pkgs-action@latest - uses: awalsh128/cache-apt-pkgs-action@latest
with: with:
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
version: 1.0 version: 1.0
- uses: dtolnay/rust-toolchain@1.88.0 - uses: dtolnay/rust-toolchain@1.94.0
with: with:
components: rustfmt, clippy components: rustfmt, clippy
- uses: cargo-bins/cargo-binstall@main
- name: Install cargo-msrv
run: cargo binstall cargo-msrv -y --force
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
cache-all-crates: "true" cache-all-crates: "true"
- uses: browser-actions/setup-firefox@latest cache-workspace-crates: "true"
- run: cargo test --lib --bins --tests --examples --workspace --exclude dioxus-desktop cache-provider: "warpbuild"
# save-if: ${{ github.ref == 'refs/heads/main' }}
release-test: - name: Rustfmt
if: github.event.pull_request.draft == false run: cargo fmt --all -- --check
name: Test Suite with Optimizations - name: Clippy
runs-on: blacksmith-4vcpu-ubuntu-2404 run: cargo clippy --workspace --examples --tests --all-features --all-targets -- -D warnings
steps: - name: Check schema
- uses: actions/checkout@v5
- name: Free Disk Space
uses: ./.github/actions/free-disk-space
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
version: 1.0
- uses: dtolnay/rust-toolchain@1.88.0
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
- uses: browser-actions/setup-firefox@latest
- run: cargo test --lib --bins --tests --examples --workspace --exclude dioxus-desktop --profile release-unoptimized
fmt:
if: github.event.pull_request.draft == false
name: Rustfmt
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.88.0
with:
components: rustfmt
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
- run: cargo fmt --all -- --check
schema:
if: github.event.pull_request.draft == false
name: Check Schema
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.88.0
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
- name: Generate schema
run: cargo run -p dioxus-cli -- config schema --out packages/cli/schema.json run: cargo run -p dioxus-cli -- config schema --out packages/cli/schema.json
- name: Check for uncommitted changes - name: Verify schema is up to date
run: | run: |
if ! git diff --exit-code packages/cli/schema.json; then if ! git diff --exit-code packages/cli/schema.json; then
echo "::error::Schema is out of date. Run 'dx config schema --out packages/cli/schema.json' and commit the changes." echo "::error::Schema is out of date. Run 'dx config schema --out packages/cli/schema.json' and commit the changes."
exit 1 exit 1
fi fi
- name: Verify MSRV
run: cargo msrv --output-format json verify -- cargo check
- name: Find MSRV on verify failure
if: ${{ failure() }}
run: cargo msrv --output-format json -- cargo check
docs: docs:
if: github.event.pull_request.draft == false needs: check-paths
name: Docs if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
runs-on: blacksmith-4vcpu-ubuntu-2404 name: "Docs | Build"
runs-on: warp-ubuntu-latest-x64-4x
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: awalsh128/cache-apt-pkgs-action@latest - uses: awalsh128/cache-apt-pkgs-action@latest
@@ -155,6 +143,9 @@ jobs:
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
cache-all-crates: "true" cache-all-crates: "true"
cache-workspace-crates: "true"
cache-provider: "warpbuild"
# save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "doc --lib --all-features" - name: "doc --lib --all-features"
run: | run: |
cargo doc --workspace --no-deps --all-features --document-private-items cargo doc --workspace --no-deps --all-features --document-private-items
@@ -162,9 +153,10 @@ jobs:
RUSTDOCFLAGS: -Dwarnings --document-private-items RUSTDOCFLAGS: -Dwarnings --document-private-items
test-docs: test-docs:
if: github.event.pull_request.draft == false needs: check-paths
name: Test Docs if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
runs-on: blacksmith-4vcpu-ubuntu-2404 name: "Docs | Test"
runs-on: warp-ubuntu-latest-x64-4x
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: awalsh128/cache-apt-pkgs-action@latest - uses: awalsh128/cache-apt-pkgs-action@latest
@@ -178,83 +170,213 @@ jobs:
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
cache-all-crates: "true" cache-all-crates: "true"
cache-workspace-crates: "true"
cache-provider: "warpbuild"
# save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "doc --lib --all-features" - name: "doc --lib --all-features"
run: | run: |
cargo test --doc --workspace --all-features cargo test --doc --workspace --all-features
check: bundle-linux-smoke:
if: github.event.pull_request.draft == false needs: check-paths
name: Check if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
runs-on: blacksmith-4vcpu-ubuntu-2404 name: "Bundle | Linux"
runs-on: warp-ubuntu-latest-x64-4x
timeout-minutes: 45
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Free Disk Space
uses: ./.github/actions/free-disk-space
- uses: awalsh128/cache-apt-pkgs-action@latest - uses: awalsh128/cache-apt-pkgs-action@latest
with: with:
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev patchelf desktop-file-utils librsvg2-dev
version: 1.0 version: 1.0
- uses: dtolnay/rust-toolchain@1.88.0 - uses: dtolnay/rust-toolchain@1.88.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
cache-all-crates: "true" cache-all-crates: "true"
- run: cargo check --workspace --all-features --all-targets cache-workspace-crates: "true"
cache-provider: "warpbuild"
# save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Bundle hotdog as Linux packages
working-directory: ./examples/01-app-demos/hotdog
run: cargo run --manifest-path ../../../Cargo.toml -p dioxus-cli -- bundle --platform desktop --package-types appimage --package-types deb --package-types rpm
- name: Verify Linux bundle outputs
run: |
find target -name '*.AppImage' -print
find target -name '*.deb' -print
find target -name '*.rpm' -print
test -n "$(find target -name '*.AppImage' -print -quit)"
test -n "$(find target -name '*.deb' -print -quit)"
test -n "$(find target -name '*.rpm' -print -quit)"
- uses: actions/upload-artifact@v6
if: always()
with:
name: hotdog-linux-bundles
path: |
target/**/*.AppImage
target/**/*.deb
target/**/*.rpm
if-no-files-found: ignore
clippy: bundle-macos-smoke:
if: github.event.pull_request.draft == false needs: check-paths
name: Clippy if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
runs-on: blacksmith-4vcpu-ubuntu-2404 name: "Bundle | macOS"
runs-on: warp-macos-latest-arm64-6x
timeout-minutes: 45
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: awalsh128/cache-apt-pkgs-action@latest - uses: dtolnay/rust-toolchain@1.88.0
- uses: Swatinem/rust-cache@v2
with: with:
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev cache-all-crates: "true"
version: 1.0 cache-workspace-crates: "true"
- uses: dtolnay/rust-toolchain@1.90.0 cache-provider: "warpbuild"
# save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Bundle hotdog as macOS packages
working-directory: ./examples/01-app-demos/hotdog
run: cargo run --manifest-path ../../../Cargo.toml -p dioxus-cli -- bundle --platform desktop --package-types macos --package-types dmg
- name: Verify macOS bundle outputs
run: |
find target -type d -name '*.app' -print
find target -type f -name '*.dmg' -print
test -n "$(find target -type d -name '*.app' -print -quit)"
test -n "$(find target -type f -name '*.dmg' -print -quit)"
- uses: actions/upload-artifact@v6
if: always()
with:
name: hotdog-macos-bundles
path: |
target/**/*.app
target/**/*.dmg
if-no-files-found: ignore
windows-bundle-smoke:
needs: check-paths
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
name: "Bundle | Windows"
runs-on: warp-windows-2025-x64-8x
timeout-minutes: 45
steps:
- uses: actions/checkout@v5
- name: Setup Dev Drive
run: ${{ github.workspace }}/.github/workflows/setup-dev-drive.ps1
- name: Copy Repo to Dev Drive
run: Copy-Item -Path "${{ github.workspace }}" -Destination "$env:DIOXUS_WORKSPACE" -Recurse
- name: Install Rust
uses: dtolnay/rust-toolchain@1.88.0
with: with:
components: rustfmt, clippy components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
key: "windows-bundle-${{ runner.os }}"
cache-all-crates: "true" cache-all-crates: "true"
- run: cargo clippy --workspace --examples --tests --all-features --all-targets -- -D warnings cache-workspace-crates: "true"
cache-provider: "warpbuild"
workspaces: ${{ env.DIOXUS_WORKSPACE }}
- name: Bundle hotdog as Windows packages
working-directory: ${{ env.DIOXUS_WORKSPACE }}/examples/01-app-demos/hotdog
run: cargo run --release --manifest-path ..\..\..\Cargo.toml -p dioxus-cli -- bundle --platform desktop --package-types msi --package-types nsis
- name: Verify Windows bundle outputs
working-directory: ${{ env.DIOXUS_WORKSPACE }}
shell: pwsh
run: |
$msi = Get-ChildItem -Path .\target\dx -Recurse -File -Include "*.msi"
$nsis = Get-ChildItem -Path .\target\dx -Recurse -File -Include "*-setup.exe"
$msi | ForEach-Object { $_.FullName }
$nsis | ForEach-Object { $_.FullName }
if (-not $msi) { throw "Expected MSI artifact was not produced." }
if (-not $nsis) { throw "Expected NSIS artifact was not produced." }
- name: Target dir size by extension
shell: pwsh
run: |
Get-ChildItem -Path "$env:DIOXUS_WORKSPACE\target" -Recurse -File |
Group-Object Extension |
Sort-Object { ($_.Group | Measure-Object Length -Sum).Sum } -Descending |
Select-Object -First 20 @{N='Ext';E={$_.Name}}, Count, @{N='SizeMB';E={[math]::Round(($_.Group | Measure-Object Length -Sum).Sum / 1MB, 1)}} |
Format-Table -AutoSize
# - name: Clean Windows artifacts before cache save
# shell: pwsh
# run: |
# Get-ChildItem -Path "$env:DIOXUS_WORKSPACE\target" -Recurse -Include "*.pdb","*.exe","*.dll" | Remove-Item -Force
- uses: actions/upload-artifact@v6
if: always()
with:
name: hotdog-windows-bundles-smoke
path: |
${{ env.DIOXUS_WORKSPACE }}/target/**/*.msi
${{ env.DIOXUS_WORKSPACE }}/target/**/*-setup.exe
if-no-files-found: ignore
nix: windows:
if: github.event.pull_request.draft == false needs: check-paths
runs-on: ${{ matrix.os }} if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
strategy: name: "Playwright | Windows"
matrix: runs-on: warp-windows-2025-x64-8x
os: [blacksmith-4vcpu-ubuntu-2404, macos-latest] timeout-minutes: 60
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: nixbuild/nix-quick-install-action@master - name: Setup Dev Drive
- uses: nix-community/cache-nix-action@main run: ${{ github.workspace }}/.github/workflows/setup-dev-drive.ps1
- name: Copy Repo to Dev Drive
run: Copy-Item -Path "${{ github.workspace }}" -Destination "$env:DIOXUS_WORKSPACE" -Recurse
- uses: actions/setup-node@v4
with: with:
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} node-version: 24
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }} - name: Install Rust
- name: Install omnix uses: dtolnay/rust-toolchain@1.88.0
run: nix --accept-flake-config profile install "github:juspay/omnix" with:
- name: Build all flake outputs targets: wasm32-unknown-unknown
run: om ci components: rustfmt, clippy
- name: Ensure devShell has all build deps - uses: Swatinem/rust-cache@v2
run: nix develop -c cargo build -p dioxus-cli --features no-downloads with:
cache-all-crates: "true"
cache-workspace-crates: "true"
cache-provider: "warpbuild"
workspaces: ${{ env.DIOXUS_WORKSPACE }}
# save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Playwright
working-directory: ${{ env.DIOXUS_WORKSPACE }}/packages/playwright-tests
env:
CARGO_INCREMENTAL: 1
run: |
npm ci
npm install -D @playwright/test
npx playwright install
npx playwright test
# - name: Clean Windows artifacts before cache save
# if: always()
# shell: pwsh
# run: |
# Get-ChildItem -Path "$env:DIOXUS_WORKSPACE\target" -Recurse -Include "*.pdb","*.exe","*.dll" | Remove-Item -Force
- uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report-windows
path: ${{ env.DIOXUS_WORKSPACE }}/packages/playwright-tests/playwright-report/
retention-days: 30
playwright: playwright:
if: github.event.pull_request.draft == false needs: check-paths
name: Playwright Tests if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
name: "Playwright | ${{ matrix.name }}"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 45 timeout-minutes: 45
strategy: strategy:
matrix: matrix:
os: [blacksmith-4vcpu-windows-2025, blacksmith-8vcpu-ubuntu-2404] include:
platform: - { os: warp-ubuntu-latest-x64-8x, name: "Linux (stable)", toolchain: "1.88.0" }
- { toolchain: beta } - { os: warp-ubuntu-latest-x64-8x, name: "Linux (beta)", toolchain: beta }
# - { toolchain: 1.86.0 }
steps: steps:
# Do our best to cache the toolchain and node install steps # Do our best to cache the toolchain and node install steps
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Free Disk Space - name: Free Disk Space
if: ${{ matrix.os == 'ubuntu-24.04' || matrix.os == 'ubuntu-24.04-arm' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404-arm' || matrix.os == 'blacksmith-8vcpu-ubuntu-2404' }} if: runner.os == 'Linux'
uses: ./.github/actions/free-disk-space uses: ./.github/actions/free-disk-space
- uses: awalsh128/cache-apt-pkgs-action@latest - uses: awalsh128/cache-apt-pkgs-action@latest
if: ${{ matrix.os == 'ubuntu-24.04' || matrix.os == 'ubuntu-24.04-arm' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404-arm' || matrix.os == 'blacksmith-8vcpu-ubuntu-2404' }} if: runner.os == 'Linux'
with: with:
packages: glib-networking glib-networking-common glib-networking-services libaa1 libabsl20220623t64 libass9 libasyncns0 libavc1394-0 libavcodec60 libavfilter9 libavformat60 libavtp0 libavutil58 libblas3 libbluray2 libbs2b0 libcaca0 libcairo-script-interpreter2 libcdparanoia0 libchromaprint1 libcjson1 libcodec2-1.2 libdav1d7 libdc1394-25 libdca0 libdecor-0-0 libdirectfb-1.7-7t64 libdv4t64 libdvdnav4 libdvdread8t64 libfaad2 libflac12t64 libfluidsynth3 libfreeaptx0 libgav1-1 libgme0 libgraphene-1.0-0 libgsm1 libgssdp-1.6-0 libgstreamer-plugins-good1.0-0 libgtk-4-common libgupnp-1.6-0 libgupnp-igd-1.6-0 libhwy1t64 libiec61883-0 libimath-3-1-29t64 libinstpatch-1.0-2 libjack-jackd2-0 libjxl0.7 liblapack3 liblc3-1 libldacbt-enc2 liblilv-0-0 liblrdf0 libltc11 libmbedcrypto7t64 libmfx1 libmjpegutils-2.1-0t64 libmodplug1 libmp3lame0 libmpcdec6 libmpeg2encpp-2.1-0t64 libmpg123-0t64 libmplex2-2.1-0t64 libmysofa1 libneon27t64 libnice10 libogg0 libopenal-data libopenal1 libopenexr-3-1-30 libopenh264-7 libopenmpt0t64 libopenni2-0 liborc-0.4-0t64 libpipewire-0.3-0t64 libplacebo338 libpocketsphinx3 libpostproc57 libproxy1v5 libpulse0 libqrencode4 libraptor2-0 librav1e0 libraw1394-11 librist4 librubberband2 libsamplerate0 libsbc1 libsdl2-2.0-0 libsecret-common libserd-0-0 libshine3 libshout3 libsndfile1 libsndio7.0 libsord-0-0 libsoundtouch1 libsoup-3.0-0 libsoup-3.0-common libsoxr0 libspa-0.2-modules libspandsp2t64 libspeex1 libsphinxbase3t64 libsratom-0-0 libsrt1.5-gnutls libsrtp2-1 libssh-gcrypt-4 libsvtav1enc1d1 libswresample4 libswscale7 libtag1v5 libtag1v5-vanilla libtheora0 libtwolame0 libudfread0 libunibreak5 libv4l-0t64 libv4lconvert0t64 libva-drm2 libva-x11-2 libva2 libvdpau1 libvidstab1.1 libvisual-0.4-0 libvo-aacenc0 libvo-amrwbenc0 libvorbis0a libvorbisenc2 libvorbisfile3 libvpl2 libwavpack1 libwebrtc-audio-processing1 libwildmidi2 libx265-199 libxcb-xkb1 libxkbcommon-x11-0 libcups2t64 libxml2 libxml2-dev libxvidcore4 libyuv0 libzbar0t64 libzimg2 libzix-0-0 libzvbi-common libzvbi0t64 libzxing3 ocl-icd-libopencl1 timgm6mb-soundfont xfonts-encodings xfonts-utils binutils lld binutils-devel binutils-gold fonts-freefont-ttf fonts-ipafont-gothic fonts-tlwg-loma-otf fonts-unifont fonts-wqy-zenhei gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-good libavif16 libevent-2.1-7t64 libgstreamer-plugins-bad1.0-0 libharfbuzz-icu0 libhyphen0 libmanette-0.2-0 libsecret-1-0 libwoff1 xfonts-cyrillic xfonts-scalable fonts-ipafont-mincho fonts-tlwg-loma gstreamer1.0-x packages: glib-networking glib-networking-common glib-networking-services libaa1 libabsl20220623t64 libass9 libasyncns0 libavc1394-0 libavcodec60 libavfilter9 libavformat60 libavtp0 libavutil58 libblas3 libbluray2 libbs2b0 libcaca0 libcairo-script-interpreter2 libcdparanoia0 libchromaprint1 libcjson1 libcodec2-1.2 libdav1d7 libdc1394-25 libdca0 libdecor-0-0 libdirectfb-1.7-7t64 libdv4t64 libdvdnav4 libdvdread8t64 libfaad2 libflac12t64 libfluidsynth3 libfreeaptx0 libgav1-1 libgme0 libgraphene-1.0-0 libgsm1 libgssdp-1.6-0 libgstreamer-plugins-good1.0-0 libgtk-4-common libgupnp-1.6-0 libgupnp-igd-1.6-0 libhwy1t64 libiec61883-0 libimath-3-1-29t64 libinstpatch-1.0-2 libjack-jackd2-0 libjxl0.7 liblapack3 liblc3-1 libldacbt-enc2 liblilv-0-0 liblrdf0 libltc11 libmbedcrypto7t64 libmfx1 libmjpegutils-2.1-0t64 libmodplug1 libmp3lame0 libmpcdec6 libmpeg2encpp-2.1-0t64 libmpg123-0t64 libmplex2-2.1-0t64 libmysofa1 libneon27t64 libnice10 libogg0 libopenal-data libopenal1 libopenexr-3-1-30 libopenh264-7 libopenmpt0t64 libopenni2-0 liborc-0.4-0t64 libpipewire-0.3-0t64 libplacebo338 libpocketsphinx3 libpostproc57 libproxy1v5 libpulse0 libqrencode4 libraptor2-0 librav1e0 libraw1394-11 librist4 librubberband2 libsamplerate0 libsbc1 libsdl2-2.0-0 libsecret-common libserd-0-0 libshine3 libshout3 libsndfile1 libsndio7.0 libsord-0-0 libsoundtouch1 libsoup-3.0-0 libsoup-3.0-common libsoxr0 libspa-0.2-modules libspandsp2t64 libspeex1 libsphinxbase3t64 libsratom-0-0 libsrt1.5-gnutls libsrtp2-1 libssh-gcrypt-4 libsvtav1enc1d1 libswresample4 libswscale7 libtag1v5 libtag1v5-vanilla libtheora0 libtwolame0 libudfread0 libunibreak5 libv4l-0t64 libv4lconvert0t64 libva-drm2 libva-x11-2 libva2 libvdpau1 libvidstab1.1 libvisual-0.4-0 libvo-aacenc0 libvo-amrwbenc0 libvorbis0a libvorbisenc2 libvorbisfile3 libvpl2 libwavpack1 libwebrtc-audio-processing1 libwildmidi2 libx265-199 libxcb-xkb1 libxkbcommon-x11-0 libcups2t64 libxml2 libxml2-dev libxvidcore4 libyuv0 libzbar0t64 libzimg2 libzix-0-0 libzvbi-common libzvbi0t64 libzxing3 ocl-icd-libopencl1 timgm6mb-soundfont xfonts-encodings xfonts-utils binutils lld binutils-devel binutils-gold fonts-freefont-ttf fonts-ipafont-gothic fonts-tlwg-loma-otf fonts-unifont fonts-wqy-zenhei gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-good libavif16 libevent-2.1-7t64 libgstreamer-plugins-bad1.0-0 libharfbuzz-icu0 libhyphen0 libmanette-0.2-0 libsecret-1-0 libwoff1 xfonts-cyrillic xfonts-scalable fonts-ipafont-mincho fonts-tlwg-loma gstreamer1.0-x
version: 1.0 version: 1.0
@@ -264,15 +386,17 @@ jobs:
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
toolchain: ${{ matrix.platform.toolchain }} toolchain: ${{ matrix.toolchain }}
targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
key: "playwright-${{ matrix.platform.toolchain }}-${{ runner.os }}" key: "playwright-${{ matrix.toolchain }}-${{ runner.os }}"
cache-all-crates: "true" cache-all-crates: "true"
cache-on-failure: "true" cache-workspace-crates: "true"
cache-provider: "warpbuild"
# save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Wipe dx cache - name: Wipe dx cache
if: ${{ matrix.os == 'ubuntu-24.04' || matrix.os == 'ubuntu-24.04-arm' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404-arm' || matrix.os == 'blacksmith-8vcpu-ubuntu-2404' }} if: runner.os == 'Linux'
run: | run: |
rm -rf ./target/dx rm -rf ./target/dx
- name: Playwright - name: Playwright
@@ -288,13 +412,67 @@ jobs:
- uses: actions/upload-artifact@v6 - uses: actions/upload-artifact@v6
if: always() if: always()
with: with:
name: playwright-report-${{ matrix.platform.toolchain }}-${{ runner.os }} name: playwright-report-${{ matrix.toolchain }}-${{ runner.os }}
path: ./packages/playwright-tests/playwright-report/ path: ./packages/playwright-tests/playwright-report/
retention-days: 30 retention-days: 30
test:
needs: check-paths
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
name: "Test | Linux"
runs-on: warp-ubuntu-latest-x64-8x
steps:
- uses: actions/checkout@v5
- name: Free Disk Space
uses: ./.github/actions/free-disk-space
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
version: 1.0
- uses: dtolnay/rust-toolchain@1.88.0
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
cache-workspace-crates: "true"
cache-provider: "warpbuild"
# save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: browser-actions/setup-firefox@latest
- run: cargo test --lib --bins --tests --examples --workspace --exclude dioxus-desktop
release-test:
needs: check-paths
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
name: "Test | Linux (Release)"
runs-on: warp-ubuntu-latest-x64-8x
steps:
- uses: actions/checkout@v5
- name: Free Disk Space
uses: ./.github/actions/free-disk-space
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
version: 1.0
- uses: dtolnay/rust-toolchain@1.88.0
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
cache-workspace-crates: "true"
cache-provider: "warpbuild"
# save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: browser-actions/setup-firefox@latest
- run: cargo test --lib --bins --tests --examples --workspace --exclude dioxus-desktop --profile release-unoptimized
matrix_test: matrix_test:
needs: check-paths
runs-on: ${{ matrix.platform.os }} runs-on: ${{ matrix.platform.os }}
if: github.event.pull_request.draft == false if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
name: "Cross | ${{ matrix.platform.target }}"
env: env:
RUST_CARGO_COMMAND: ${{ matrix.platform.cross == true && 'cross' || 'cargo' }} RUST_CARGO_COMMAND: ${{ matrix.platform.cross == true && 'cross' || 'cargo' }}
strategy: strategy:
@@ -302,7 +480,7 @@ jobs:
platform: platform:
- { - {
target: aarch64-apple-darwin, target: aarch64-apple-darwin,
os: macos-latest, os: warp-macos-latest-arm64-6x,
toolchain: "1.88.0", toolchain: "1.88.0",
cross: false, cross: false,
command: "test", command: "test",
@@ -311,7 +489,7 @@ jobs:
} }
- { - {
target: aarch64-apple-ios, target: aarch64-apple-ios,
os: macos-latest, os: warp-macos-latest-arm64-6x,
toolchain: "1.88.0", toolchain: "1.88.0",
cross: false, cross: false,
command: "build", command: "build",
@@ -320,7 +498,7 @@ jobs:
} }
- { - {
target: aarch64-unknown-linux-gnu, target: aarch64-unknown-linux-gnu,
os: ubuntu-24.04-arm, os: warp-ubuntu-latest-arm64-4x,
toolchain: "1.88.0", toolchain: "1.88.0",
cross: false, cross: false,
command: "build", command: "build",
@@ -329,14 +507,14 @@ jobs:
} }
- { - {
target: aarch64-linux-android, target: aarch64-linux-android,
os: blacksmith-4vcpu-ubuntu-2404, os: warp-ubuntu-latest-x64-4x,
toolchain: "1.88.0", toolchain: "1.88.0",
cross: true, cross: true,
command: "build", command: "build",
args: "--package dioxus-desktop", args: "--package dioxus-desktop",
platform: "android", platform: "android",
} }
# commented out because it's having issues with space on the device, but we already test it above # commented out because it's having issues with space on the device, but we already test it above
# - { # - {
# target: x86_64-unknown-linux-gnu, # target: x86_64-unknown-linux-gnu,
# os: ubuntu-24.04, # os: ubuntu-24.04,
@@ -350,15 +528,31 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Free Disk Space - name: Free Disk Space
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404-arm' }} if: runner.os == 'Linux'
uses: ./.github/actions/free-disk-space uses: ./.github/actions/free-disk-space
- uses: awalsh128/cache-apt-pkgs-action@latest - uses: awalsh128/cache-apt-pkgs-action@latest
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404-arm' }} if: ${{ runner.os == 'Linux' && matrix.platform.target != 'aarch64-unknown-linux-gnu' }}
with: with:
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev packages: pkg-config libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
version: ${{ matrix.platform.target }}-${{ matrix.platform.os }} # disambiguate since we're in a matrix and this caching action doesn't factor in these variables version: ${{ matrix.platform.target }}-${{ matrix.platform.os }} # disambiguate since we're in a matrix and this caching action doesn't factor in these variables
- name: Install Linux desktop dependencies for ARM
if: ${{ matrix.platform.target == 'aarch64-unknown-linux-gnu' }}
run: |
sudo apt-get update
sudo apt-get install -y pkg-config libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
- name: Verify GLib pkg-config metadata on ARM Linux
if: ${{ matrix.platform.target == 'aarch64-unknown-linux-gnu' }}
run: pkg-config --modversion glib-2.0
- name: Configure pkg-config for ARM Linux desktop deps
if: ${{ matrix.platform.target == 'aarch64-unknown-linux-gnu' }}
run: |
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig" >> "$GITHUB_ENV"
echo "PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig" >> "$GITHUB_ENV"
- name: install stable - name: install stable
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
@@ -366,7 +560,7 @@ jobs:
targets: ${{ matrix.platform.target }} targets: ${{ matrix.platform.target }}
components: rustfmt components: rustfmt
- name: Install nasm for windows (tls) - name: Install nasm for windows (tls)1
if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }} if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }}
uses: ilammy/setup-nasm@v1 uses: ilammy/setup-nasm@v1
@@ -378,33 +572,45 @@ jobs:
with: with:
key: "matrix-${{ matrix.platform.target }}" key: "matrix-${{ matrix.platform.target }}"
cache-all-crates: "true" cache-all-crates: "true"
cache-workspace-crates: "true"
cache-provider: "warpbuild"
# save-if: ${{ github.ref == 'refs/heads/main' }}
- name: test - name: test
run: | run: |
${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }} ${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}
# borrowed from uv nix:
# https://raw.githubusercontent.com/astral-sh/uv/refs/heads/main/.github/workflows/ci.yml needs: check-paths
cargo-test-windows: if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
if: github.event.pull_request.draft == false name: "Nix | ${{ matrix.name }}"
runs-on: runs-on: ${{ matrix.os }}
labels: "blacksmith-4vcpu-windows-2025" strategy:
name: "cargo test | windows" matrix:
include:
- { os: warp-ubuntu-latest-x64-8x, name: Linux }
- { os: warp-macos-latest-arm64-6x, name: macOS }
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.88.0 - uses: nixbuild/nix-quick-install-action@master
- uses: jkelleyrtp/cache-nix-action@jk/use-warpbuild
with: with:
components: rustfmt, clippy primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.nix', 'flake.lock') }}-${{ hashFiles('Cargo.lock') }}
- uses: Swatinem/rust-cache@v2 restore-prefixes-first-match: |
with: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.nix', 'flake.lock') }}-
workspaces: ${{ env.UV_WORKSPACE }} nix-${{ runner.os }}-${{ runner.arch }}-
cache-all-crates: "true" backend: warpbuild
- name: Build dioxus-cli from the flake
- name: "Install Rust toolchain" run: nix build .#dioxus-cli
working-directory: ${{ env.UV_WORKSPACE }} - name: Smoke test devShell
run: rustup show shell: bash
- name: "Cargo test"
working-directory: ${{ env.UV_WORKSPACE }}
run: | run: |
cargo test --workspace --tests nix develop .#default -c bash -lc '
cargo --version
rustc --version
pkg-config --modversion openssl
if [[ "${{ runner.os }}" == "Linux" ]]; then
pkg-config --modversion gtk+-3.0
pkg-config --modversion webkit2gtk-4.1
fi
'
+3 -2
View File
@@ -23,7 +23,7 @@ jobs:
# Attempt to backport a merged pull request to the latest stable release # Attempt to backport a merged pull request to the latest stable release
backport: backport:
name: Backport pull request name: Backport pull request
runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: warp-ubuntu-latest-x64-4x
# Don't run on closed unmerged pull requests, or pull requests with the "breaking" label # Don't run on closed unmerged pull requests, or pull requests with the "breaking" label
if: github.event.pull_request.merged && !contains(github.event.pull_request.labels.*.name, 'breaking') if: github.event.pull_request.merged && !contains(github.event.pull_request.labels.*.name, 'breaking')
@@ -34,7 +34,7 @@ jobs:
# Upload nightly docs to the website # Upload nightly docs to the website
docs: docs:
runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: warp-ubuntu-latest-x64-4x
permissions: permissions:
contents: write contents: write
steps: steps:
@@ -47,6 +47,7 @@ jobs:
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
cache-all-crates: "true" cache-all-crates: "true"
cache-provider: "warpbuild"
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo doc - name: cargo doc
+1 -1
View File
@@ -22,7 +22,7 @@ permissions:
jobs: jobs:
promote: promote:
runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: warp-ubuntu-latest-x64-4x
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Publish the next pre-release - name: Publish the next pre-release
+12 -2
View File
@@ -84,11 +84,21 @@ jobs:
uses: ./.github/actions/free-disk-space uses: ./.github/actions/free-disk-space
- uses: awalsh128/cache-apt-pkgs-action@latest - uses: awalsh128/cache-apt-pkgs-action@latest
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }} if: ${{ matrix.platform.os == 'ubuntu-24.04' }}
with: with:
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev musl-tools packages: pkg-config libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev musl-tools
version: 1.0 version: 1.0
- name: Install Linux desktop dependencies for ARM
if: ${{ matrix.platform.os == 'ubuntu-24.04-arm' }}
run: |
sudo apt-get update
sudo apt-get install -y pkg-config libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev musl-tools
- name: Verify GLib pkg-config metadata on ARM Linux
if: ${{ matrix.platform.os == 'ubuntu-24.04-arm' }}
run: pkg-config --modversion glib-2.0
- name: Install stable - name: Install stable
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
+69 -21
View File
@@ -1,26 +1,74 @@
# This creates a 20GB dev drive, and exports all required environment # Configures a dev drive for Windows CI to speed up Rust builds.
# variables so that rustup, uv and others all use the dev drive as much #
# as possible. # The main performance win is disabling Windows Defender antivirus scanning
$Volume = New-VHD -Path C:/uv_dev_drive.vhdx -SizeBytes 20GB | # on the build drive — AV scanning every .o, .rlib, .rmeta, and .exe during
Mount-VHD -Passthru | # a Rust build is extremely expensive.
Initialize-Disk -Passthru | #
New-Partition -AssignDriveLetter -UseMaximumSize | # Three code paths:
Format-Volume -FileSystem ReFS -Confirm:$false -Force # 1. D: drive exists (some runners) — use it directly
# 2. Hyper-V available — create a Dev Drive VHD (best perf, supports fsutil devdrv)
# 3. No Hyper-V (Warp/EC2 runners) — use diskpart + ReFS format
Write-Output $Volume if (Test-Path "D:\") {
Write-Output "Using existing drive at D:"
$Drive = "D:"
} elseif (Get-Command New-VHD -ErrorAction SilentlyContinue) {
# Hyper-V is available — create a proper Dev Drive
$Volume = New-VHD -Path C:/dev_drive.vhdx -SizeBytes 25GB |
Mount-VHD -Passthru |
Initialize-Disk -Passthru |
New-Partition -AssignDriveLetter -UseMaximumSize |
Format-Volume -DevDrive -Confirm:$false -Force
$Drive = "$($Volume.DriveLetter):" $Drive = "$($Volume.DriveLetter):"
$Tmp = "$($Drive)/uv-tmp"
# Create the directory ahead of time in an attempt to avoid race-conditions # Mark as trusted and disable antivirus filtering
New-Item $Tmp -ItemType Directory fsutil devdrv trust $Drive
fsutil devdrv enable /disallowAv
# Remount so the changes take effect
Dismount-VHD -Path C:/dev_drive.vhdx
Mount-VHD -Path C:/dev_drive.vhdx
Write-Output $Volume
fsutil devdrv query $Drive
Write-Output "Created Dev Drive at $Drive"
} else {
# No Hyper-V — fall back to diskpart + ReFS
Write-Output "No Hyper-V detected, creating ReFS drive via diskpart..."
$vhdPath = "C:\dev_drive.vhdx"
@"
create vdisk file="$vhdPath" maximum=25600 type=expandable
attach vdisk
create partition primary
active
assign letter=V
"@ | diskpart
format V: /fs:ReFS /q /y
$Drive = "V:"
Write-Output "Created ReFS drive at $Drive"
}
$Tmp = "$($Drive)\tmp"
# Create directories ahead of time to avoid race conditions
New-Item $Tmp -ItemType Directory -Force
# Move Cargo to the dev drive so registry/crate downloads also benefit
New-Item -Path "$($Drive)/.cargo/bin" -ItemType Directory -Force
if (Test-Path "C:/Users/runneradmin/.cargo") {
Copy-Item -Path "C:/Users/runneradmin/.cargo/*" -Destination "$($Drive)/.cargo/" -Recurse -Force
}
Write-Output ` Write-Output `
"DEV_DRIVE=$($Drive)" ` "DEV_DRIVE=$($Drive)" `
"TMP=$($Tmp)" ` "TMP=$($Tmp)" `
"TEMP=$($Tmp)" ` "TEMP=$($Tmp)" `
"RUSTUP_HOME=$($Drive)/.rustup" ` "RUSTUP_HOME=$($Drive)/.rustup" `
"CARGO_HOME=$($Drive)/.cargo" ` "CARGO_HOME=$($Drive)/.cargo" `
"UV_WORKSPACE=$($Drive)/uv" ` "DIOXUS_WORKSPACE=$($Drive)/dioxus" `
"PATH=$($Drive)/.cargo/bin;$env:PATH" ` "PATH=$($Drive)/.cargo/bin;$env:PATH" `
>> $env:GITHUB_ENV >> $env:GITHUB_ENV
-42
View File
@@ -1,42 +0,0 @@
# Whenever an open PR is updated, the workflow will be triggered
name: Language Linting
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches:
- main
jobs:
typos:
if: github.event.pull_request.draft == false
name: Check for typos
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- name: Check for typos
uses: crate-ci/typos@master
# Check for invalid links in the repository
link-check:
if: github.event.pull_request.draft == false
name: Check For Invalid Links
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v5
- name: Restore lychee cache
uses: actions/cache@v5
with:
path: .lycheecache
key: cache-lychee-${{ github.sha }}
restore-keys: cache-lychee-
- name: Run lychee
uses: lycheeverse/lychee-action@v2
with:
args: --config ./lychee.toml './**/*.md'
fail: true
Generated
+1082 -3775
View File
File diff suppressed because it is too large. Load diff
+15 -11
View File
@@ -5,7 +5,6 @@ members = [
"packages/core", "packages/core",
"packages/core-types", "packages/core-types",
"packages/cli", "packages/cli",
"packages/cli-opt",
"packages/cli-config", "packages/cli-config",
"packages/cli-telemetry", "packages/cli-telemetry",
"packages/core-macro", "packages/core-macro",
@@ -22,7 +21,6 @@ members = [
"packages/interpreter", "packages/interpreter",
"packages/liveview", "packages/liveview",
"packages/autofmt", "packages/autofmt",
"packages/check",
"packages/devtools-types", "packages/devtools-types",
"packages/devtools", "packages/devtools",
"packages/document", "packages/document",
@@ -140,7 +138,6 @@ dioxus-desktop = { path = "packages/desktop", version = "0.7.3", default-feature
dioxus-interpreter-js = { path = "packages/interpreter", version = "0.7.3" } dioxus-interpreter-js = { path = "packages/interpreter", version = "0.7.3" }
dioxus-liveview = { path = "packages/liveview", version = "0.7.3" } dioxus-liveview = { path = "packages/liveview", version = "0.7.3" }
dioxus-autofmt = { path = "packages/autofmt", version = "0.7.3" } dioxus-autofmt = { path = "packages/autofmt", version = "0.7.3" }
dioxus-check = { path = "packages/check", version = "0.7.3" }
dioxus-rsx = { path = "packages/rsx", version = "0.7.3" } dioxus-rsx = { path = "packages/rsx", version = "0.7.3" }
dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.7.3" } dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.7.3" }
dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.7.3" } dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.7.3" }
@@ -164,7 +161,6 @@ generational-box = { path = "packages/generational-box", version = "0.7.3" }
lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.7.3" } lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.7.3" }
# cli # cli
dioxus-cli-opt = { path = "packages/cli-opt", version = "0.7.3" }
dioxus-cli-telemetry = { path = "packages/cli-telemetry", version = "0.7.3" } dioxus-cli-telemetry = { path = "packages/cli-telemetry", version = "0.7.3" }
dioxus-cli-config = { path = "packages/cli-config", version = "0.7.3" } dioxus-cli-config = { path = "packages/cli-config", version = "0.7.3" }
@@ -229,7 +225,7 @@ futures = "0.3.31"
futures-channel = "0.3.31" futures-channel = "0.3.31"
futures-util = { version = "0.3", default-features = false } futures-util = { version = "0.3", default-features = false }
rustc-hash = "2.1.1" rustc-hash = "2.1.1"
wasm-bindgen = "0.2.105" wasm-bindgen = "0.2.114"
wasm-bindgen-futures = "0.4.50" wasm-bindgen-futures = "0.4.50"
js-sys = "0.3" js-sys = "0.3"
web-sys = { version = "0.3.77", default-features = false } web-sys = { version = "0.3.77", default-features = false }
@@ -238,9 +234,12 @@ thiserror = "2.0.12"
prettyplease = { version = "0.2.35", features = ["verbatim"] } prettyplease = { version = "0.2.35", features = ["verbatim"] }
const_format = "0.2.34" const_format = "0.2.34"
cargo_toml = { version = "0.22.1" } cargo_toml = { version = "0.22.1" }
tauri-utils = { version = "=2.5.0" } icns = "0.4.0"
tauri-macos-sign = { version = "=2.2.0" } image = "0.25"
tauri-bundler = { version = "=2.5.0" } zip = { version = "4", default-features = false, features = ["deflate"] }
sha1 = "0.10"
md5 = "0.8.0"
rpm = { version = "0.19.0", default-features = false, features = ["gzip-compression"] }
lru = "0.16.0" lru = "0.16.0"
async-trait = "0.1.88" async-trait = "0.1.88"
axum = { version = "0.8.4", default-features = false } axum = { version = "0.8.4", default-features = false }
@@ -282,8 +281,8 @@ ciborium = "0.2.2"
base64 = "0.22.1" base64 = "0.22.1"
uuid = "1.17.0" uuid = "1.17.0"
convert_case = "0.8.0" convert_case = "0.8.0"
tungstenite = { version = "0.27.0" } tungstenite = { version = "0.28.0" }
tokio-tungstenite = { version = "0.27.0" } tokio-tungstenite = { version = "0.28.0" }
gloo-timers = "0.3.0" gloo-timers = "0.3.0"
internment = { version = "0.8.6" } internment = { version = "0.8.6" }
proc-macro2-diagnostics = { version = "0.10", default-features = false } proc-macro2-diagnostics = { version = "0.10", default-features = false }
@@ -359,7 +358,7 @@ gloo-dialogs = "0.2.0"
ansi-to-tui = "7.0" ansi-to-tui = "7.0"
ansi-to-html = "0.2.2" ansi-to-html = "0.2.2"
path-absolutize = "3.1" path-absolutize = "3.1"
crossterm = { version = "0.29.0" } crossterm = { version = "0.28.1" }
ratatui = { version = "0.29.0" } ratatui = { version = "0.29.0" }
shell-words = "1.1.0" shell-words = "1.1.0"
@@ -367,6 +366,11 @@ shell-words = "1.1.0"
keyboard-types = { version = "0.7", default-features = false } keyboard-types = { version = "0.7", default-features = false }
winit = { version = "0.30.11", features = ["rwh_06"] } winit = { version = "0.30.11", features = ["rwh_06"] }
# reduce debug symbols to line tables only - speeds up compiles and shrinks artifacts
# backtraces still show file:line, but debugger can't inspect variables
[profile.dev]
debug = "line-tables-only"
# our release profile should be fast to compile and fast to run # our release profile should be fast to compile and fast to run
# when we ship our CI builds, we turn on LTO which improves perf leftover by turning on incremental # when we ship our CI builds, we turn on LTO which improves perf leftover by turning on incremental
[profile.release] [profile.release]
+2
View File
@@ -10,6 +10,8 @@ seeked = "seeked"
udid = "udid" udid = "udid"
# Part of Blitz's API # Part of Blitz's API
unparented = "unparented" unparented = "unparented"
# bundle type / archive
flate2="flate2"
[files] [files]
extend-exclude = ["notes/translations/*", "CHANGELOG.md", "*.js"] extend-exclude = ["notes/translations/*", "CHANGELOG.md", "*.js"]
+1 -1
View File
@@ -10,7 +10,7 @@ dioxus = { workspace = true, features = ["fullstack", "router"] }
reqwest = { workspace = true, features = ["json"] } reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
rusqlite = { version = "0.32.0", optional = true } rusqlite = { version = "0.32.0", optional = true, features = ["bundled"] } # Bundle SQLite so Windows/MSVC builds do not require an external sqlite3.lib.
anyhow = { workspace = true } anyhow = { workspace = true }
[features] [features]
Generated
+16
View File
@@ -1,5 +1,20 @@
{ {
"nodes": { "nodes": {
"crane": {
"locked": {
"lastModified": 1773189535,
"narHash": "sha256-E1G/Or6MWeP+L6mpQ0iTFLpzSzlpGrITfU2220Gq47g=",
"owner": "ipetkov",
"repo": "crane",
"rev": "6fa2fb4cf4a89ba49fc9dd5a3eb6cde99d388269",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-parts": { "flake-parts": {
"inputs": { "inputs": {
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": "nixpkgs-lib"
@@ -67,6 +82,7 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"crane": "crane",
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay", "rust-overlay": "rust-overlay",
+42 -39
View File
@@ -5,8 +5,7 @@
systems.url = "github:nix-systems/default"; systems.url = "github:nix-systems/default";
rust-overlay.url = "github:oxalica/rust-overlay"; rust-overlay.url = "github:oxalica/rust-overlay";
# crane.url = "github:ipetkov/crane"; crane.url = "github:ipetkov/crane";
# crane.inputs.nixpkgs.follows = "nixpkgs";
}; };
outputs = outputs =
@@ -16,14 +15,18 @@
perSystem = perSystem =
{ {
config,
self', self',
pkgs,
lib,
system, system,
... ...
}: }:
let let
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [
inputs.rust-overlay.overlays.default
];
};
lib = pkgs.lib;
rustToolchain = pkgs.rust-bin.stable.latest.default.override { rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [ extensions = [
"rust-src" "rust-src"
@@ -31,6 +34,7 @@
"clippy" "clippy"
]; ];
}; };
craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain;
rustBuildInputs = [ rustBuildInputs = [
pkgs.openssl pkgs.openssl
pkgs.libiconv pkgs.libiconv
@@ -49,6 +53,26 @@
]; ];
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
fullSrc = pkgs.lib.cleanSource ./.;
cargoSrc = craneLib.cleanCargoSource fullSrc;
commonArgs = {
src = fullSrc;
strictDeps = true;
buildInputs = rustBuildInputs;
nativeBuildInputs = [
pkgs.pkg-config
];
};
cargoArtifacts = craneLib.buildDepsOnly (
commonArgs
// {
src = cargoSrc;
pname = "dioxus-deps";
version = cargoToml.package.version;
}
);
rustPackage = rustPackage =
package: package:
@@ -56,53 +80,32 @@
binary ? package, binary ? package,
features ? [ ], features ? [ ],
}: }:
(pkgs.makeRustPlatform { craneLib.buildPackage (
cargo = rustToolchain; commonArgs
rustc = rustToolchain; // {
}).buildRustPackage
{
pname = package; pname = package;
version = cargoToml.package.version; version = cargoToml.package.version;
src = pkgs.lib.cleanSource ./.; inherit cargoArtifacts;
cargoLock.lockFile = ./Cargo.lock; cargoExtraArgs = "--locked --package ${package} ${
buildInputs = rustBuildInputs; lib.concatStringsSep " " (map (f: "--features ${f}") features)
nativeBuildInputs = [ }";
rustToolchain doCheck = false; # Disable tests to avoid building deps for them
pkgs.pkg-config installPhaseCommand = ''
];
buildPhase = ''
mkdir -p .cargo
cp ${./Cargo.lock} Cargo.lock
cargo build --release --package ${package} ${
lib.concatStringsSep " " (map (f: "--features ${f}") features)
}
'';
installPhase = ''
mkdir -p $out/bin mkdir -p $out/bin
ls -alR target/release
cp target/release/${binary} $out/bin/ cp target/release/${binary} $out/bin/
''; '';
doCheck = false; # Disable tests to avoid building deps for them }
}; );
# This is useful when building crates as packages
# Note that it does require a `Cargo.lock` which this repo does not have
# craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain;
in in
{ {
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
overlays = [
inputs.rust-overlay.overlays.default
];
};
packages.dioxus-cli = ( packages.dioxus-cli = (
rustPackage "dioxus-cli" { rustPackage "dioxus-cli" {
binary = "dx"; binary = "dx";
features = [ "no-downloads" ]; features = [ "no-downloads" ];
} }
); );
packages.default = self'.packages.dioxus-cli;
checks.dioxus-cli = self'.packages.dioxus-cli;
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
name = "dioxus-dev"; name = "dioxus-dev";
+1 -1
View File
@@ -135,7 +135,7 @@ Blitz combines a number of exciting projects to bring customizable HTML renderin
Blitz is an extremely capable renderer, often producing results indistinguishable from browsers like Chrome and Safari: Blitz is an extremely capable renderer, often producing results indistinguishable from browsers like Chrome and Safari:
Not every CSS feature is supported yet, with some bugs like incorrect writing direction or the occasional layout quirk. Our support matrix is here: https://blitz-website.fly.dev/status/css Not every CSS feature is supported yet, with some bugs like incorrect writing direction or the occasional layout quirk.
The samples that Blitz can create are quite incredible. Servo’s website: The samples that Blitz can create are quite incredible. Servo’s website:
+16 -17
View File
@@ -145,15 +145,15 @@ pub fn serve_asset(path: &str) -> Result<Response<Vec<u8>>, AssetServeError> {
/// - [x] iOS /// - [x] iOS
/// - [x] Windows /// - [x] Windows
/// - [x] Linux (appimage) /// - [x] Linux (appimage)
/// - [ ] Linux (rpm) /// - [x] Linux (rpm)
/// - [x] Linux (deb) /// - [x] Linux (deb)
/// - [ ] Android /// - [x] Android (returns current exe.)
#[allow(unreachable_code)] #[allow(unreachable_code)]
fn get_asset_root() -> PathBuf { fn get_asset_root() -> PathBuf {
let cur_exe = std::env::current_exe().unwrap(); let cur_exe = std::env::current_exe().unwrap();
#[cfg(target_os = "macos")] // In macOS bundles, the assets are in the `Resources` directory
{ if cfg!(target_os = "macos") {
return cur_exe return cur_exe
.parent() .parent()
.unwrap() .unwrap()
@@ -162,20 +162,19 @@ fn get_asset_root() -> PathBuf {
.join("Resources"); .join("Resources");
} }
#[cfg(target_os = "linux")] // In linux bundles, the assets are placed in the lib/$product_name directory
{ // bin/
// In linux bundles, the assets are placed in the lib/$product_name directory // main
// bin/ // lib/
// main // $product_name/
// lib/ // assets/
// $product_name/ if cfg!(target_os = "linux") {
// assets/
if let Some(product_name) = dioxus_cli_config::product_name() { if let Some(product_name) = dioxus_cli_config::product_name() {
let lib_asset_path = || { let path = cur_exe
let path = cur_exe.parent()?.parent()?.join("lib").join(product_name); .parent()
path.exists().then_some(path) .and_then(|parent| parent.parent())
}; .map(|root| root.join("lib").join(product_name));
if let Some(asset_dir) = lib_asset_path() { if let Some(asset_dir) = path.filter(|path| path.exists()) {
return asset_dir; return asset_dir;
} }
} }
-20
View File
@@ -1,20 +0,0 @@
[package]
name = "dioxus-check"
version = { workspace = true }
edition = "2021"
authors = ["Dioxus Labs"]
description = "Checks Dioxus RSX files for issues"
license = "MIT/Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
keywords = ["web", "desktop", "mobile", "gui", "wasm"]
[dependencies]
proc-macro2 = { workspace = true, features = ["span-locations"] }
quote = {workspace = true }
syn = { workspace = true, features = ["full", "extra-traits", "visit"] }
owo-colors = { workspace = true, features = ["supports-colors"] }
[dev-dependencies]
indoc = "2.0.6"
pretty_assertions = { workspace = true }
-39
View File
@@ -1,39 +0,0 @@
# dioxus-check
[![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-autofmt.svg
[crates-url]: https://crates.io/crates/dioxus-check
[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.7/) |
[API Docs](https://docs.rs/dioxus-check) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
`dioxus-check` analyzes Dioxus source code and reports errors and warnings. Primarily, it enforces the [Rules of Hooks](https://dioxuslabs.com/learn/0.7/essentials/basics/hooks#rules-of-hooks).
## 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.
-10
View File
@@ -1,10 +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")]
mod check;
mod issues;
mod metadata;
pub use check::check_file;
pub use issues::{Issue, IssueReport};
-81
View File
@@ -1,81 +0,0 @@
[package]
name = "dioxus-cli-opt"
edition = "2021"
version.workspace = true
authors = ["Jonathan Kelley", "Evan Almloff"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
description = "CLI optimizations for dioxus-cli"
keywords = ["dom", "ui", "gui", "react"]
[dependencies]
anyhow = { workspace = true }
manganis = { workspace = true }
manganis-core = { workspace = true }
object = { workspace = true, features = ["wasm"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
const-serialize = { workspace = true, features = ["serde"] }
tracing = { workspace = true }
rayon = "1.10.0"
# Image compression/conversion
# - JPEG
mozjpeg = { version = "0.10.13", default-features = false, features = [
"parallel",
] }
# - PNG
imagequant = "4.4.0"
png = "0.17.16"
# Image format/conversion
image = { version = "0.25", features = ["avif"] }
# CSS Minification
lightningcss = { version = "1.0.0-alpha.67", features = [
"browserslist",
"into_owned",
] }
# SCSS Processing
grass = "0.13.4"
codemap = "0.1.3"
# Js minification - swc has introduces minor versions with breaking changes in the past so we pin all of their crates
# Root dependencies
swc_bundler = { version = "=24.0.0", default-features = false }
swc_common = { version = "=13.0.4", features = [
"tty-emitter",
], default-features = false }
swc_ecma_ast = { version = "=13.0.1", default-features = false }
swc_ecma_codegen = { version = "=15.0.2", default-features = false }
swc_ecma_loader = { version = "=13.0.0", features = [
"cache",
"node",
], default-features = false }
swc_ecma_minifier = { version = "=25.0.0", default-features = false }
swc_ecma_parser = { version = "=19.0.0", default-features = false }
swc_ecma_transforms_base = { version = "=20.0.0", default-features = false }
swc_ecma_visit = { version = "=13.0.0", default-features = false }
# Pinned from cargo tree --prefix none | grep -Eo 'swc[^ ]+ [^ ]+' | sort -u
swc_allocator = { version = "4.0.1" }
swc_atoms = { version = "6.0.1" }
swc_config = { version = "3.1.1" }
swc_config_macro = { version = "1.0.1" }
swc_ecma_codegen_macros = { version = "2.0.2" }
swc_ecma_lexer = { version = "19.0.1" }
swc_ecma_transforms_optimization = { version = "21.0.0" }
swc_ecma_usage_analyzer = { version = "19.0.2" }
swc_ecma_utils = { version = "18.1.0" }
swc_eq_ignore_macros = { version = "1.0.1" }
swc_graph_analyzer = { version = "14.0.1" }
swc_macros_common = { version = "1.0.1" }
swc_sourcemap = { version = "9.3.2" }
swc_timer = { version = "1.0.0" }
swc_visit = { version = "2.0.1" }
browserslist-rs = { version = "=0.19.0" }
[build-dependencies]
built = { version = "0.8.0", features = ["git2"] }
-3
View File
@@ -1,3 +0,0 @@
fn main() {
built::write_built_file().expect("Failed to acquire build-time information");
}
-10
View File
@@ -1,10 +0,0 @@
// The file has been placed there by the build script.
include!(concat!(env!("OUT_DIR"), "/built.rs"));
pub(crate) fn version() -> String {
format!(
"{} ({})",
PKG_VERSION,
GIT_COMMIT_HASH_SHORT.unwrap_or("was built without git repository")
)
}
-23
View File
@@ -1,23 +0,0 @@
use image::{DynamicImage, EncodableLayout};
use std::{
io::{BufWriter, Write},
path::Path,
};
pub(crate) fn compress_jpg(image: DynamicImage, output_location: &Path) -> anyhow::Result<()> {
let mut comp = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_EXT_RGBX);
let width = image.width() as usize;
let height = image.height() as usize;
comp.set_size(width, height);
let mut comp = comp.start_compress(Vec::new())?; // any io::Write will work
comp.write_scanlines(image.to_rgba8().as_bytes())?;
let jpeg_bytes = comp.finish()?;
let file = std::fs::File::create(output_location)?;
let w = &mut BufWriter::new(file);
w.write_all(&jpeg_bytes)?;
Ok(())
}
-52
View File
@@ -1,52 +0,0 @@
use std::{io::BufWriter, path::Path};
use image::DynamicImage;
pub(crate) fn compress_png(image: DynamicImage, output_location: &Path) {
// Image loading/saving is outside scope of this library
let width = image.width() as usize;
let height = image.height() as usize;
let bitmap: Vec<_> = image
.into_rgba8()
.pixels()
.map(|px| imagequant::RGBA::new(px[0], px[1], px[2], px[3]))
.collect();
// Configure the library
let mut liq = imagequant::new();
liq.set_speed(5).unwrap();
liq.set_quality(0, 99).unwrap();
// Describe the bitmap
let mut img = liq.new_image(&bitmap[..], width, height, 0.0).unwrap();
// The magic happens in quantize()
let mut res = match liq.quantize(&mut img) {
Ok(res) => res,
Err(err) => panic!("Quantization failed, because: {err:?}"),
};
let (palette, pixels) = res.remapped(&mut img).unwrap();
let file = std::fs::File::create(output_location).unwrap();
let w = &mut BufWriter::new(file);
let mut encoder = png::Encoder::new(w, width as u32, height as u32);
encoder.set_color(png::ColorType::Rgba);
let mut flattened_palette = Vec::new();
let mut alpha_palette = Vec::new();
for px in palette {
flattened_palette.push(px.r);
flattened_palette.push(px.g);
flattened_palette.push(px.b);
alpha_palette.push(px.a);
}
encoder.set_palette(flattened_palette);
encoder.set_trns(alpha_palette);
encoder.set_depth(png::BitDepth::Eight);
encoder.set_color(png::ColorType::Indexed);
encoder.set_compression(png::Compression::Best);
let mut writer = encoder.write_header().unwrap();
writer.write_image_data(&pixels).unwrap();
writer.finish().unwrap();
}
-295
View File
@@ -1,295 +0,0 @@
use std::hash::Hasher;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Context;
use manganis_core::JsAssetOptions;
use swc_common::errors::Emitter;
use swc_common::errors::Handler;
use swc_common::input::SourceFileInput;
use swc_ecma_minifier::option::{ExtraOptions, MinifyOptions};
use swc_ecma_parser::lexer::Lexer;
use swc_ecma_parser::Parser;
use swc_ecma_transforms_base::fixer::fixer;
use swc_ecma_visit::VisitMutWith;
use std::collections::HashMap;
use anyhow::Error;
use swc_bundler::{Bundler, Config, Load, ModuleData, ModuleRecord};
use swc_common::{
errors::HANDLER, sync::Lrc, FileName, FilePathMapping, Globals, Mark, SourceMap, Span, GLOBALS,
};
use swc_ecma_ast::*;
use swc_ecma_codegen::text_writer::JsWriter;
use swc_ecma_loader::{resolvers::node::NodeModulesResolver, TargetEnv};
use swc_ecma_parser::{parse_file_as_module, Syntax};
use crate::hash::hash_file_contents;
struct TracingEmitter;
impl Emitter for TracingEmitter {
fn emit(&mut self, db: &mut swc_common::errors::DiagnosticBuilder<'_>) {
match db.level {
swc_common::errors::Level::Bug
| swc_common::errors::Level::Fatal
| swc_common::errors::Level::PhaseFatal
| swc_common::errors::Level::Error => tracing::error!("{}", db.message()),
swc_common::errors::Level::Warning
| swc_common::errors::Level::FailureNote
| swc_common::errors::Level::Cancelled => tracing::warn!("{}", db.message()),
swc_common::errors::Level::Note | swc_common::errors::Level::Help => {
tracing::trace!("{}", db.message())
}
}
}
}
/// Run a closure with the swc globals and handler set up
fn inside_handler<O>(f: impl FnOnce(&Globals, Lrc<SourceMap>) -> O) -> O {
let globals = Globals::new();
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
let handler = Handler::with_emitter_and_flags(Box::new(TracingEmitter), Default::default());
GLOBALS.set(&globals, || HANDLER.set(&handler, || f(&globals, cm)))
}
fn bundle_js_to_writer(
file: PathBuf,
bundle: bool,
minify: bool,
write_to: &mut impl std::io::Write,
) -> anyhow::Result<()> {
inside_handler(|globals, cm| {
bundle_js_to_writer_inside_handler(globals, cm, file, bundle, minify, write_to)
})
}
fn resolve_js_inside_handler(
globals: &Globals,
file: PathBuf,
bundle: bool,
cm: &Lrc<SourceMap>,
) -> anyhow::Result<Module> {
if bundle {
let node_resolver = NodeModulesResolver::new(TargetEnv::Browser, Default::default(), true);
let mut bundler = Bundler::new(
globals,
cm.clone(),
PathLoader { cm: cm.clone() },
node_resolver,
Config {
require: true,
..Default::default()
},
Box::new(Hook),
);
let mut entries = HashMap::default();
entries.insert("main".to_string(), FileName::Real(file));
let mut bundles = bundler
.bundle(entries)
.context("failed to bundle javascript with swc")?;
// Since we only inserted one entry, there should only be one bundle in the output
let bundle = bundles
.pop()
.ok_or_else(|| anyhow::anyhow!("swc did not output any bundles"))?;
Ok(bundle.module)
} else {
let fm = cm.load_file(Path::new(&file)).expect("Failed to load file");
let lexer = Lexer::new(
Default::default(),
Default::default(),
SourceFileInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
parser.parse_module().map_err(|err| {
HANDLER.with(|handler| {
let mut error = err.into_diagnostic(handler);
// swc errors panic on drop if you don't cancel them
error.cancel();
anyhow::anyhow!("{}", error.message())
})
})
}
}
fn bundle_js_to_writer_inside_handler(
globals: &Globals,
cm: Lrc<SourceMap>,
file: PathBuf,
bundle: bool,
minify: bool,
write_to: &mut impl std::io::Write,
) -> anyhow::Result<()> {
let mut module = resolve_js_inside_handler(globals, file, bundle, &cm)?;
if minify {
module = swc_ecma_minifier::optimize(
std::mem::take(&mut module).into(),
cm.clone(),
None,
None,
&MinifyOptions {
rename: true,
compress: None,
mangle: None,
..Default::default()
},
&ExtraOptions {
unresolved_mark: Mark::new(),
top_level_mark: Mark::new(),
mangle_name_cache: None,
},
)
.expect_module();
module.visit_mut_with(&mut fixer(None));
}
let mut emitter = swc_ecma_codegen::Emitter {
cfg: swc_ecma_codegen::Config::default().with_minify(minify),
cm: cm.clone(),
comments: None,
wr: Box::new(JsWriter::new(cm, "\n", write_to, None)),
};
emitter.emit_module(&module)?;
Ok(())
}
struct PathLoader {
cm: Lrc<SourceMap>,
}
impl Load for PathLoader {
fn load(&self, file: &FileName) -> anyhow::Result<ModuleData> {
let file = match file {
FileName::Real(v) => v,
_ => anyhow::bail!("Only real files are supported"),
};
let fm = self.cm.load_file(file)?;
let module = HANDLER.with(|handler| {
parse_file_as_module(
&fm,
Syntax::Es(Default::default()),
Default::default(),
None,
&mut Vec::new(),
)
.map_err(|err| {
let mut error = err.into_diagnostic(handler);
// swc errors panic on drop if you don't cancel them
error.cancel();
anyhow::anyhow!("{}", error.message())
})
.context("Failed to parse javascript")
})?;
Ok(ModuleData {
fm,
module,
helpers: Default::default(),
})
}
}
// Adapted from https://github.com/swc-project/swc/blob/624680b7896cef9d8e30bd5ff910538298016974/bindings/binding_core_node/src/bundle.rs#L266-L302
struct Hook;
impl swc_bundler::Hook for Hook {
fn get_import_meta_props(
&self,
span: Span,
module_record: &ModuleRecord,
) -> Result<Vec<KeyValueProp>, Error> {
let file_name = module_record.file_name.to_string();
Ok(vec![
KeyValueProp {
key: PropName::Ident(IdentName::new("url".into(), span)),
value: Box::new(Expr::Lit(Lit::Str(Str {
span,
raw: None,
value: file_name.into(),
}))),
},
KeyValueProp {
key: PropName::Ident(IdentName::new("main".into(), span)),
value: Box::new(if module_record.is_entry {
Expr::Member(MemberExpr {
span,
obj: Box::new(Expr::MetaProp(MetaPropExpr {
span,
kind: MetaPropKind::ImportMeta,
})),
prop: MemberProp::Ident(IdentName::new("main".into(), span)),
})
} else {
Expr::Lit(Lit::Bool(Bool { span, value: false }))
}),
},
])
}
}
pub(crate) fn process_js(
js_options: &JsAssetOptions,
source: &Path,
output_path: &Path,
bundle: bool,
) -> anyhow::Result<()> {
let mut writer = std::io::BufWriter::new(std::fs::File::create(output_path)?);
if js_options.minified() {
if let Err(err) = bundle_js_to_writer(source.to_path_buf(), bundle, true, &mut writer) {
tracing::error!("Failed to minify js. Falling back to non-minified: {err}");
} else {
return Ok(());
}
}
let mut source_file = std::fs::File::open(source)?;
std::io::copy(&mut source_file, &mut writer).with_context(|| {
format!(
"Failed to write js to output location: {}",
output_path.display()
)
})?;
Ok(())
}
fn hash_js_module(file: PathBuf, hasher: &mut impl Hasher, bundle: bool) -> anyhow::Result<()> {
inside_handler(|globals, cm| {
_ = resolve_js_inside_handler(globals, file, bundle, &cm)?;
for file in cm.files().iter() {
let hash = file.src_hash;
hasher.write(&hash.to_le_bytes());
}
Ok(())
})
}
pub(crate) fn hash_js(
js_options: &JsAssetOptions,
source: &Path,
hasher: &mut impl Hasher,
bundle: bool,
) -> anyhow::Result<()> {
if js_options.minified() {
if let Err(err) = hash_js_module(source.to_path_buf(), hasher, bundle) {
tracing::error!("Failed to minify js. Falling back to non-minified: {err}");
hash_file_contents(source, hasher)?;
}
} else {
hash_file_contents(source, hasher)?;
}
Ok(())
}
+23 -26
View File
@@ -7,11 +7,10 @@ description = "CLI for building fullstack web, desktop, and mobile apps with a s
repository = "https://github.com/DioxusLabs/dioxus/" repository = "https://github.com/DioxusLabs/dioxus/"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["mobile", "gui", "cli", "dioxus", "wasm"] keywords = ["mobile", "gui", "cli", "dioxus", "wasm"]
rust-version = "1.81.0" rust-version = "1.82.0"
[dependencies] [dependencies]
dioxus-autofmt = { workspace = true } dioxus-autofmt = { workspace = true }
dioxus-check = { workspace = true }
dioxus-rsx-rosetta = { workspace = true } dioxus-rsx-rosetta = { workspace = true }
dioxus-rsx = { workspace = true } dioxus-rsx = { workspace = true }
dioxus-rsx-hotreload = { workspace = true } dioxus-rsx-hotreload = { workspace = true }
@@ -20,8 +19,6 @@ dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-core-types = { workspace = true } dioxus-core-types = { workspace = true }
dioxus-devtools-types = { workspace = true } dioxus-devtools-types = { workspace = true }
dioxus-cli-config = { workspace = true } dioxus-cli-config = { workspace = true }
dioxus-cli-opt = { workspace = true }
dioxus-fullstack = { workspace = true }
dioxus-dx-wire-format = { workspace = true } dioxus-dx-wire-format = { workspace = true }
wasm-split-cli = { workspace = true } wasm-split-cli = { workspace = true }
depinfo = { workspace = true } depinfo = { workspace = true }
@@ -33,10 +30,8 @@ bytes = { workspace = true }
clap = { workspace = true, features = ["derive", "cargo"] } clap = { workspace = true, features = ["derive", "cargo"] }
convert_case = { workspace = true } convert_case = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
uuid = { workspace = true, features = ["v4"] } uuid = { workspace = true, features = ["v4", "v5", "v7"] }
# swc uses serde internals that break with newer versions of serde. We should move off of swc eventually, serde = { workspace = true, features = ["derive"] }
# but pinning serde fixes the issue in the meantime
serde = { version = "=1.0.221", features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
cargo_toml = { workspace = true, features = ["features"] } cargo_toml = { workspace = true, features = ["features"] }
@@ -61,9 +56,10 @@ console = "0.16.0"
ctrlc = "3.4.7" ctrlc = "3.4.7"
axum = { workspace = true, default-features = true, features = ["ws"] } axum = { workspace = true, default-features = true, features = ["ws"] }
axum-server = { workspace = true, features = ["tls-rustls"] } axum-server = { workspace = true, features = ["tls-rustls-no-provider"] }
axum-extra = { workspace = true, features = ["typed-header"] } axum-extra = { workspace = true, features = ["typed-header"] }
tower-http = { workspace = true, features = ["full"] } tower-http = { workspace = true, features = ["full"] }
owo-colors = { workspace = true, features = ["supports-colors"] }
proc-macro2 = { workspace = true, features = ["span-locations"] } proc-macro2 = { workspace = true, features = ["span-locations"] }
syn = { workspace = true, features = [ syn = { workspace = true, features = [
"full", "full",
@@ -72,13 +68,12 @@ syn = { workspace = true, features = [
"visit-mut", "visit-mut",
] } ] }
headers = "0.4.1"
walkdir = "2" walkdir = "2"
dunce = { workspace = true } dunce = { workspace = true }
# tools download # tools download
dirs = { workspace = true } dirs = { workspace = true }
reqwest = { workspace = true, features = ["rustls-tls", "trust-dns", "json"] } reqwest = { workspace = true, default-features = false, features = ["rustls-tls", "json"] }
tower = { workspace = true } tower = { workspace = true }
git2 = "0.20.2" git2 = "0.20.2"
@@ -95,9 +90,14 @@ toml_edit = "0.22.27"
prettyplease = { workspace = true } prettyplease = { workspace = true }
# Assets # Assets
lightningcss = { version = "1.0.0-alpha.67", features = ["browserslist", "into_owned"] }
grass = "0.13.4"
codemap = "0.1.3"
mozjpeg = { version = "0.10.13", default-features = false, features = ["parallel"] }
imagequant = "4.4.0"
png = "0.17.16"
brotli = "8.0.1" brotli = "8.0.1"
ignore = "0.4.23" ignore = "0.4.23"
env_logger = { workspace = true }
const-serialize = { workspace = true, features = ["serde"] } const-serialize = { workspace = true, features = ["serde"] }
const-serialize-07 = { package = "const-serialize", version = "=0.7.2", features = ["serde"] } const-serialize-07 = { package = "const-serialize", version = "=0.7.2", features = ["serde"] }
@@ -127,7 +127,6 @@ manganis = { workspace = true }
manganis-core = { workspace = true } manganis-core = { workspace = true }
manganis-core-07 = { workspace = true } manganis-core-07 = { workspace = true }
target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] } target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] }
wasm-encoder = "0.235.0"
# Extracting data from an executable # Extracting data from an executable
object = { workspace = true, features = ["all"] } object = { workspace = true, features = ["all"] }
@@ -136,26 +135,26 @@ itertools = { workspace = true }
throbber-widgets-tui = "0.8.0" throbber-widgets-tui = "0.8.0"
unicode-segmentation = "1.12.0" unicode-segmentation = "1.12.0"
handlebars = "6.3.2" handlebars = "6.3.2"
strum = { version = "0.27.1", features = ["derive"] }
memmap = "0.7.0"
walrus = { workspace = true, features = ["parallel"] } walrus = { workspace = true, features = ["parallel"] }
wasmparser = { workspace = true } wasmparser = { workspace = true }
tauri-utils = { workspace = true } # bundler dependencies
tauri-macos-sign = { workspace = true } icns = { workspace = true }
tauri-bundler = { workspace = true } image = { workspace = true }
zip = { workspace = true }
sha1 = { workspace = true }
sha2 = { workspace = true }
md5 = { workspace = true }
rpm = { workspace = true }
include_dir = "0.7.4" include_dir = "0.7.4"
flate2 = "1.1.2" flate2 = "1.1.2"
tar = "0.4.44" tar = "0.4.44"
local-ip-address = "0.6.5"
dircpy = "0.3.19" dircpy = "0.3.19"
plist = "1.7.4" plist = "1.7.4"
memoize = "0.5.1"
backtrace = "0.3.74" backtrace = "0.3.74"
ar = "0.9.0" ar = "0.9.0"
wasm-bindgen-externref-xform = "0.2.100"
pdb = "0.8.0" pdb = "0.8.0"
self_update = { version = "0.42.0", features = [ self_update = { version = "0.42.0", default-features = false, features = [
"archive-tar", "archive-tar",
"archive-zip", "archive-zip",
"compression-flate2", "compression-flate2",
@@ -163,7 +162,6 @@ self_update = { version = "0.42.0", features = [
] } ] }
self-replace = "1.5.0" self-replace = "1.5.0"
cargo-config2 = { workspace = true } cargo-config2 = { workspace = true }
posthog-rs = "0.3.5"
serde_json5 = "0.2.1" serde_json5 = "0.2.1"
fs2 = "0.4.3" fs2 = "0.4.3"
sentry-backtrace = "0.42.0" sentry-backtrace = "0.42.0"
@@ -175,9 +173,6 @@ home = "=0.5.11"
[target.'cfg(target_env = "musl")'.dependencies] [target.'cfg(target_env = "musl")'.dependencies]
git2 = { version = "0.20.2", features = ["vendored-libgit2"] } git2 = { version = "0.20.2", features = ["vendored-libgit2"] }
[build-dependencies]
built = { version = "0.8.0", features = ["git2"] }
[features] [features]
default = [] default = []
tokio-console = ["dep:console-subscriber", "tokio/tracing"] tokio-console = ["dep:console-subscriber", "tokio/tracing"]
@@ -190,6 +185,8 @@ name = "dx"
[dev-dependencies] [dev-dependencies]
escargot = "0.5" escargot = "0.5"
indoc = "2.0.6"
pretty_assertions = { workspace = true }
[package.metadata.binstall] [package.metadata.binstall]
pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }{ archive-suffix }" pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }{ archive-suffix }"
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+40 -1
View File
@@ -1,3 +1,42 @@
use std::process::Command;
fn main() { fn main() {
built::write_built_file().expect("Failed to acquire build-time information"); println!("cargo:rerun-if-env-changed=DIOXUS_CLI_GIT_SHA");
println!("cargo:rerun-if-env-changed=DIOXUS_CLI_GIT_SHA_SHORT");
println!("cargo:rerun-if-changed=.git/HEAD");
println!("cargo:rerun-if-changed=.git/refs");
let full_hash = std::env::var("DIOXUS_CLI_GIT_SHA")
.ok()
.or_else(resolve_git_commit_hash);
if let Some(full_hash) = full_hash {
println!("cargo:rustc-env=DIOXUS_CLI_GIT_SHA={full_hash}");
let short_hash = std::env::var("DIOXUS_CLI_GIT_SHA_SHORT")
.ok()
.unwrap_or_else(|| full_hash.chars().take(7).collect());
println!("cargo:rustc-env=DIOXUS_CLI_GIT_SHA_SHORT={short_hash}");
}
}
fn resolve_git_commit_hash() -> Option<String> {
let output = Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let hash = String::from_utf8(output.stdout).ok()?;
let hash = hash.trim().to_string();
if hash.is_empty() {
None
} else {
Some(hash)
}
} }
+2 -2
View File
@@ -3266,14 +3266,14 @@
] ]
}, },
"upgrade_code": { "upgrade_code": {
"description": "A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**, otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app.\n\nBy default, tauri generates this code by generating a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace. You can use Tauri's CLI to generate and print this code for you by running `tauri inspect wix-upgrade-code`.\n\nIt is recommended that you set this value in your tauri config file to avoid accidental changes in your upgrade code whenever you want to change your product name.", "description": "A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**, otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app.\n\nBy default, this code is generated as a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace.\n\nIt is recommended that you set this value in your Dioxus.toml to avoid accidental changes in your upgrade code whenever you want to change your product name.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"version": { "version": {
"description": "MSI installer version in the format `major.minor.patch.build` (build is optional).\n\nBecause a valid version is required for MSI installer, it will be derived from [`tauri_bundler::PackageSettings::version`] if this field is not set.\n\nThe first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255. The third and fourth fields have a maximum value of 65,535.\n\nSee <https://learn.microsoft.com/en-us/windows/win32/msi/productversion> for more info.", "description": "MSI installer version in the format `major.minor.patch.build` (build is optional).\n\nBecause a valid version is required for MSI installer, it will be derived from the package version if this field is not set.\n\nThe first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255. The third and fourth fields have a maximum value of 65,535.\n\nSee <https://learn.microsoft.com/en-us/windows/win32/msi/productversion> for more info.",
"type": [ "type": [
"string", "string",
"null" "null"
+2 -2
View File
@@ -33,10 +33,10 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use crate::opt::AssetManifest;
use crate::Result; use crate::Result;
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use const_serialize::{deserialize_const, serialize_const, ConstVec}; use const_serialize::{deserialize_const, serialize_const, ConstVec};
use dioxus_cli_opt::AssetManifest;
use manganis::{AssetOptions, AssetVariant, BundledAsset, ImageFormat, ImageSize}; use manganis::{AssetOptions, AssetVariant, BundledAsset, ImageFormat, ImageSize};
use manganis_core::{AndroidArtifactMetadata, SwiftPackageMetadata, SymbolData}; use manganis_core::{AndroidArtifactMetadata, SwiftPackageMetadata, SymbolData};
use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol}; use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol};
@@ -811,7 +811,7 @@ pub(crate) async fn extract_symbols_from_file(
// Add the hash to each asset in parallel // Add the hash to each asset in parallel
assets assets
.par_iter_mut() .par_iter_mut()
.for_each(dioxus_cli_opt::add_hash_to_asset); .for_each(crate::opt::add_hash_to_asset);
// Write back only assets to the binary file (permissions are not modified) // Write back only assets to the binary file (permissions are not modified)
for entry in write_entries { for entry in write_entries {
+10 -5
View File
@@ -1,10 +1,10 @@
use crate::opt::process_file_to;
use crate::{ use crate::{
build::cache::ObjectCache, serve::WebServer, verbosity_or_default, BuildArtifacts, build::cache::ObjectCache, serve::WebServer, verbosity_or_default, BuildArtifacts,
BuildRequest, BuildStage, BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result, BuildRequest, BuildStage, BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result,
RustcArgs, StructuredOutput, RustcArgs, StructuredOutput,
}; };
use anyhow::{bail, Context, Error}; use anyhow::{bail, Context, Error};
use dioxus_cli_opt::process_file_to;
use futures_util::{future::OptionFuture, pin_mut, FutureExt}; use futures_util::{future::OptionFuture, pin_mut, FutureExt};
use itertools::Itertools; use itertools::Itertools;
use std::{ use std::{
@@ -475,9 +475,12 @@ impl AppBuilder {
current, current,
total, total,
krate, krate,
fresh,
.. ..
} => { } => {
tracing::info!("Compiled [{current:>3}/{total}]: {krate}"); if !fresh {
tracing::info!("Compiled [{current:>3}/{total}]: {krate}");
}
} }
BuildStage::RunningBindgen => tracing::info!("Running wasm-bindgen..."), BuildStage::RunningBindgen => tracing::info!("Running wasm-bindgen..."),
BuildStage::CopyingAssets { BuildStage::CopyingAssets {
@@ -509,7 +512,7 @@ impl AppBuilder {
} }
BuilderUpdate::BuildFailed { err } => { BuilderUpdate::BuildFailed { err } => {
// Flush remaining compiler messages // Flush remaining compiler messages
while let Ok(Some(msg)) = self.rx.try_next() { while let Ok(msg) = self.rx.try_recv() {
if let BuilderUpdate::CompilerMessage { message } = msg { if let BuilderUpdate::CompilerMessage { message } = msg {
tracing::info!(json = %StructuredOutput::RustcOutput { message: message.clone() }, %message); tracing::info!(json = %StructuredOutput::RustcOutput { message: message.clone() }, %message);
} }
@@ -760,7 +763,8 @@ impl AppBuilder {
let to = asset_dir.join(bundled.bundled_path()); let to = asset_dir.join(bundled.bundled_path());
tracing::debug!("Copying asset from patch: {}", from.display()); tracing::debug!("Copying asset from patch: {}", from.display());
if let Err(e) = dioxus_cli_opt::process_file_to(bundled.options(), &from, &to) { let esbuild = crate::esbuild::Esbuild::path_if_installed();
if let Err(e) = process_file_to(bundled.options(), &from, &to, esbuild.as_deref()) {
tracing::error!("Failed to copy asset: {e}"); tracing::error!("Failed to copy asset: {e}");
continue; continue;
} }
@@ -869,7 +873,8 @@ impl AppBuilder {
// the asset would be in a new location because the contents and hash have changed. Since we are // the asset would be in a new location because the contents and hash have changed. Since we are
// hotreloading, we need to use the old asset location it was originally written to. // hotreloading, we need to use the old asset location it was originally written to.
let options = *resource.options(); let options = *resource.options();
let res = process_file_to(&options, &changed_file, &output_path); let esbuild = crate::esbuild::Esbuild::path_if_installed();
let res = process_file_to(&options, &changed_file, &output_path, esbuild.as_deref());
let bundled_name = PathBuf::from(resource.bundled_path()); let bundled_name = PathBuf::from(resource.bundled_path());
if let Err(e) = res { if let Err(e) = res {
tracing::debug!("Failed to hotreload asset {e}"); tracing::debug!("Failed to hotreload asset {e}");
+8 -1
View File
@@ -137,12 +137,19 @@ impl BuildContext {
tracing::trace!(dx_src = ?TraceSrc::Cargo, "{line}"); tracing::trace!(dx_src = ?TraceSrc::Cargo, "{line}");
} }
pub(crate) fn status_build_progress(&self, count: usize, total: usize, name: String) { pub(crate) fn status_build_progress(
&self,
count: usize,
total: usize,
name: String,
fresh: bool,
) {
_ = self.tx.unbounded_send(BuilderUpdate::Progress { _ = self.tx.unbounded_send(BuilderUpdate::Progress {
stage: BuildStage::Compiling { stage: BuildStage::Compiling {
current: count, current: count,
total, total,
krate: name, krate: name,
fresh,
}, },
}); });
} }
+1 -1
View File
@@ -144,9 +144,9 @@ pub async fn create_framework_bundle(
std::fs::write(resources_dir.join("Info.plist"), info_plist)?; std::fs::write(resources_dir.join("Info.plist"), info_plist)?;
// Create symbolic links (required for macOS framework structure) // Create symbolic links (required for macOS framework structure)
let versions_dir = framework_dir.join("Versions");
#[cfg(unix)] #[cfg(unix)]
{ {
let versions_dir = framework_dir.join("Versions");
std::os::unix::fs::symlink("A", versions_dir.join("Current"))?; std::os::unix::fs::symlink("A", versions_dir.join("Current"))?;
std::os::unix::fs::symlink( std::os::unix::fs::symlink(
format!("Versions/Current/{}", framework_name), format!("Versions/Current/{}", framework_name),
+1 -1
View File
@@ -8,7 +8,7 @@
//! Eventually, we might want to expand this to include more metadata about the build, including //! Eventually, we might want to expand this to include more metadata about the build, including
//! build time, target platform, etc. //! build time, target platform, etc.
use dioxus_cli_opt::AssetManifest; use crate::opt::AssetManifest;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Default, Serialize, Deserialize)] #[derive(Default, Serialize, Deserialize)]
+19 -2
View File
@@ -1420,8 +1420,25 @@ fn name_is_bindgen_symbol(name: &str) -> bool {
|| name.contains("__wbindgen_externref") || name.contains("__wbindgen_externref")
|| name.contains("wasm_bindgen8describe6inform") || name.contains("wasm_bindgen8describe6inform")
|| name.contains("wasm_bindgen..describe..WasmDescribe") || name.contains("wasm_bindgen..describe..WasmDescribe")
|| name.contains("wasm_bindgen..closure..WasmClosure$GT$8describe") || (name.contains("wasm_bindgen..closure..WasmClosure") && name.contains("describe"))
|| name.contains("wasm_bindgen7closure16Closure$LT$T$GT$4wrap8describe") || (name.contains("wasm_bindgen7closure16Closure") && name.contains("describe"))
|| (name.contains("wasm_bindgen7convert8closures") && name.contains("describe_invoke"))
}
// Test for bindgen symbols. As we find more bad symbols, add them here
#[test]
fn bindgen_symbol_catch() {
let symbol = "_ZN12wasm_bindgen7convert8closures1_142_$LT$impl$u20$wasm_bindgen..closure..WasmClosure$u20$for$u20$dyn$u20$core..ops..function..FnMut$LT$$LP$$RP$$GT$$u2b$Output$u20$$u3d$$u20$R$GT$15describe_invoke17h4373f8b6570333dcE";
assert!(name_is_bindgen_symbol(symbol));
// matches_legacy_wasm_bindgen_closure_describe_symbols
let symbol = "_ZN12wasm_bindgen7closure16Closure$LT$T$GT$4wrap8describe17h1234567890abcdefE";
assert!(name_is_bindgen_symbol(symbol));
// does_not_match_saved_runtime_exports
assert!(!name_is_bindgen_symbol("__wbindgen_malloc"));
assert!(!name_is_bindgen_symbol("__wbindgen_realloc"));
assert!(!name_is_bindgen_symbol("__wbindgen_free"));
} }
/// Manually parse the data section from a wasm module /// Manually parse the data section from a wasm module
+32 -4
View File
@@ -320,6 +320,7 @@
//! - xbuild: <https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs> //! - xbuild: <https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs>
use super::HotpatchModuleCache; use super::HotpatchModuleCache;
use crate::opt::{process_file_to, AssetManifest};
use crate::{ use crate::{
AndroidTools, AppManifest, BuildContext, BuildId, BundleFormat, DioxusConfig, Error, AndroidTools, AppManifest, BuildContext, BuildId, BundleFormat, DioxusConfig, Error,
LinkAction, LinkerFlavor, ObjectCache, Platform, Renderer, Result, RustcArgs, TargetArgs, LinkAction, LinkerFlavor, ObjectCache, Platform, Renderer, Result, RustcArgs, TargetArgs,
@@ -331,7 +332,6 @@ use cargo_toml::{Profile, Profiles, StripSetting};
use depinfo::RustcDepInfo; use depinfo::RustcDepInfo;
use dioxus_cli_config::{format_base_path_meta_element, PRODUCT_NAME_ENV}; use dioxus_cli_config::{format_base_path_meta_element, PRODUCT_NAME_ENV};
use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV}; use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV};
use dioxus_cli_opt::{process_file_to, AssetManifest};
use itertools::Itertools; use itertools::Itertools;
use krates::{cm::TargetKind, NodeId}; use krates::{cm::TargetKind, NodeId};
use manganis::{AssetOptions, BundledAsset, SwiftPackageMetadata}; use manganis::{AssetOptions, BundledAsset, SwiftPackageMetadata};
@@ -1306,7 +1306,12 @@ impl BuildRequest {
} }
Message::CompilerArtifact(artifact) => { Message::CompilerArtifact(artifact) => {
units_compiled += 1; units_compiled += 1;
ctx.status_build_progress(units_compiled, crate_count, artifact.target.name); ctx.status_build_progress(
units_compiled,
crate_count,
artifact.target.name,
artifact.fresh,
);
output_location = artifact.executable.map(Into::into); output_location = artifact.executable.map(Into::into);
} }
// todo: this can occasionally swallow errors, so we should figure out what exactly is going wrong // todo: this can occasionally swallow errors, so we should figure out what exactly is going wrong
@@ -2280,6 +2285,7 @@ impl BuildRequest {
// Parallel Copy over the assets and keep track of progress with an atomic counter // Parallel Copy over the assets and keep track of progress with an atomic counter
let progress = ctx.tx.clone(); let progress = ctx.tx.clone();
let ws_dir = self.workspace_dir(); let ws_dir = self.workspace_dir();
let esbuild_path = crate::esbuild::Esbuild::path_if_installed();
// Optimizing assets is expensive and blocking, so we do it in a tokio spawn blocking task // Optimizing assets is expensive and blocking, so we do it in a tokio spawn blocking task
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
@@ -2292,7 +2298,7 @@ impl BuildRequest {
"Starting asset copy {processing}/{asset_count} from {from_:?}" "Starting asset copy {processing}/{asset_count} from {from_:?}"
); );
let res = process_file_to(options, from, to); let res = process_file_to(options, from, to, esbuild_path.as_deref());
if let Err(err) = res.as_ref() { if let Err(err) = res.as_ref() {
tracing::error!("Failed to copy asset {from:?}: {err}"); tracing::error!("Failed to copy asset {from:?}: {err}");
} }
@@ -5524,6 +5530,25 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
.join("app-debug.apk") .join("app-debug.apk")
} }
pub(crate) fn release_apk_path(&self) -> PathBuf {
self.root_dir()
.join("app")
.join("build")
.join("outputs")
.join("apk")
.join("release")
.join("app-release.apk")
}
pub(crate) fn android_apk_path(&self) -> PathBuf {
let assembled_release = self.release && self.config.bundle.android.is_some();
if assembled_release {
self.release_apk_path()
} else {
self.debug_apk_path()
}
}
/// We only really currently care about: /// We only really currently care about:
/// ///
/// - app dir (.app, .exe, .apk, etc) /// - app dir (.app, .exe, .apk, etc)
@@ -5683,6 +5708,9 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
self.verify_toolchain_installed().await?; self.verify_toolchain_installed().await?;
// esbuild is used for JS asset processing on all platforms
let _esbuild_path = crate::esbuild::Esbuild::get_or_install().await?;
match self.bundle { match self.bundle {
BundleFormat::Web => self.verify_web_tooling().await?, BundleFormat::Web => self.verify_web_tooling().await?,
BundleFormat::Ios => self.verify_ios_tooling().await?, BundleFormat::Ios => self.verify_ios_tooling().await?,
@@ -5970,7 +5998,7 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
if let Some(name) = entry.file_name().to_str() { if let Some(name) = entry.file_name().to_str() {
if name.starts_with(&prefix) && name.ends_with(".rlib") { if name.starts_with(&prefix) && name.ends_with(".rlib") {
let mtime = entry.metadata().ok()?.modified().ok()?; let mtime = entry.metadata().ok()?.modified().ok()?;
if best.as_ref().map_or(true, |(_, t)| mtime > *t) { if best.as_ref().is_none_or(|(_, t)| mtime > *t) {
best = Some((entry.path(), mtime)); best = Some((entry.path(), mtime));
} }
} }
-194
View File
@@ -1,194 +0,0 @@
use crate::{
config::BundleConfig, CustomSignCommandSettings, DebianSettings, MacOsSettings,
NSISInstallerMode, NsisSettings, PackageType, WebviewInstallMode, WindowsSettings, WixSettings,
};
impl From<NsisSettings> for tauri_bundler::NsisSettings {
fn from(val: NsisSettings) -> Self {
tauri_bundler::NsisSettings {
header_image: val.header_image,
sidebar_image: val.sidebar_image,
installer_icon: val.installer_icon,
install_mode: val.install_mode.into(),
languages: val.languages,
display_language_selector: val.display_language_selector,
custom_language_files: None,
template: None,
compression: tauri_utils::config::NsisCompression::None,
start_menu_folder: val.start_menu_folder,
installer_hooks: val.installer_hooks,
minimum_webview2_version: val.minimum_webview2_version,
}
}
}
impl From<BundleConfig> for tauri_bundler::BundleSettings {
fn from(val: BundleConfig) -> Self {
tauri_bundler::BundleSettings {
identifier: val.identifier,
publisher: val.publisher,
icon: val.icon,
resources: val.resources,
copyright: val.copyright,
category: val.category.and_then(|c| c.parse().ok()),
short_description: val.short_description,
long_description: val.long_description,
external_bin: val.external_bin,
deb: val.deb.map(Into::into).unwrap_or_default(),
macos: val.macos.map(Into::into).unwrap_or_default(),
windows: val.windows.map(Into::into).unwrap_or_default(),
..Default::default()
}
}
}
impl From<DebianSettings> for tauri_bundler::DebianSettings {
fn from(val: DebianSettings) -> Self {
tauri_bundler::DebianSettings {
depends: val.depends,
files: val.files,
desktop_template: val.desktop_template,
provides: val.provides,
conflicts: val.conflicts,
replaces: val.replaces,
section: val.section,
priority: val.priority,
changelog: val.changelog,
pre_install_script: val.pre_install_script,
post_install_script: val.post_install_script,
pre_remove_script: val.pre_remove_script,
post_remove_script: val.post_remove_script,
recommends: val.recommends,
}
}
}
impl From<WixSettings> for tauri_bundler::WixSettings {
fn from(val: WixSettings) -> Self {
tauri_bundler::WixSettings {
language: tauri_bundler::bundle::WixLanguage({
let mut languages: Vec<_> = val
.language
.iter()
.map(|l| {
(
l.0.clone(),
tauri_bundler::bundle::WixLanguageConfig {
locale_path: l.1.clone(),
},
)
})
.collect();
if languages.is_empty() {
languages.push(("en-US".into(), Default::default()));
}
languages
}),
template: val.template,
fragment_paths: val.fragment_paths,
component_group_refs: val.component_group_refs,
component_refs: val.component_refs,
feature_group_refs: val.feature_group_refs,
feature_refs: val.feature_refs,
merge_refs: val.merge_refs,
enable_elevated_update_task: val.enable_elevated_update_task,
banner_path: val.banner_path,
dialog_image_path: val.dialog_image_path,
fips_compliant: val.fips_compliant,
version: val.version,
upgrade_code: val.upgrade_code,
}
}
}
impl From<MacOsSettings> for tauri_bundler::MacOsSettings {
fn from(val: MacOsSettings) -> Self {
tauri_bundler::MacOsSettings {
frameworks: val.frameworks,
minimum_system_version: val.minimum_system_version,
exception_domain: val.exception_domain,
signing_identity: val.signing_identity,
provider_short_name: val.provider_short_name,
entitlements: val.entitlements,
info_plist_path: val.info_plist_path,
files: val.files,
hardened_runtime: val.hardened_runtime,
bundle_version: val.bundle_version,
bundle_name: val.bundle_name,
}
}
}
#[allow(deprecated)]
impl From<WindowsSettings> for tauri_bundler::WindowsSettings {
fn from(val: WindowsSettings) -> Self {
tauri_bundler::WindowsSettings {
digest_algorithm: val.digest_algorithm,
certificate_thumbprint: val.certificate_thumbprint,
timestamp_url: val.timestamp_url,
tsp: val.tsp,
wix: val.wix.map(Into::into),
webview_install_mode: val.webview_install_mode.into(),
allow_downgrades: val.allow_downgrades,
nsis: val.nsis.map(Into::into),
sign_command: val.sign_command.map(Into::into),
icon_path: val.icon_path.unwrap_or("./icons/icon.ico".into()),
}
}
}
impl From<NSISInstallerMode> for tauri_utils::config::NSISInstallerMode {
fn from(val: NSISInstallerMode) -> Self {
match val {
NSISInstallerMode::CurrentUser => tauri_utils::config::NSISInstallerMode::CurrentUser,
NSISInstallerMode::PerMachine => tauri_utils::config::NSISInstallerMode::PerMachine,
NSISInstallerMode::Both => tauri_utils::config::NSISInstallerMode::Both,
}
}
}
impl From<PackageType> for tauri_bundler::PackageType {
fn from(value: PackageType) -> Self {
match value {
PackageType::MacOsBundle => Self::MacOsBundle,
PackageType::IosBundle => Self::IosBundle,
PackageType::WindowsMsi => Self::WindowsMsi,
PackageType::Deb => Self::Deb,
PackageType::Rpm => Self::Rpm,
PackageType::AppImage => Self::AppImage,
PackageType::Dmg => Self::Dmg,
PackageType::Updater => Self::Updater,
PackageType::Nsis => Self::Nsis,
}
}
}
impl WebviewInstallMode {
fn into(self) -> tauri_utils::config::WebviewInstallMode {
match self {
Self::Skip => tauri_utils::config::WebviewInstallMode::Skip,
Self::DownloadBootstrapper { silent } => {
tauri_utils::config::WebviewInstallMode::DownloadBootstrapper { silent }
}
Self::EmbedBootstrapper { silent } => {
tauri_utils::config::WebviewInstallMode::EmbedBootstrapper { silent }
}
Self::OfflineInstaller { silent } => {
tauri_utils::config::WebviewInstallMode::OfflineInstaller { silent }
}
Self::FixedRuntime { path } => {
tauri_utils::config::WebviewInstallMode::FixedRuntime { path }
}
}
}
}
impl From<CustomSignCommandSettings> for tauri_bundler::CustomSignCommandSettings {
fn from(val: CustomSignCommandSettings) -> Self {
tauri_bundler::CustomSignCommandSettings {
cmd: val.cmd,
args: val.args,
}
}
}
+125
View File
@@ -0,0 +1,125 @@
use super::{copy_dir_recursive, zip_dir_recursive, Arch, Bundle, BundleContext};
use crate::PackageType;
use anyhow::{bail, Context, Result};
use std::path::{Path, PathBuf};
use tokio::process::Command;
pub(crate) struct IosBundled {
pub ipa: Vec<PathBuf>,
#[allow(dead_code)]
pub app: Vec<PathBuf>,
}
impl BundleContext<'_> {
/// Return the already-assembled iOS `.app` bundle produced by the build pipeline.
pub(crate) async fn bundle_ios_app(&self) -> Result<Vec<PathBuf>> {
let app_path = self.build.root_dir();
if !app_path.exists() {
bail!(
"iOS app bundle not found at {}. Ensure the iOS build completed successfully.",
app_path.display()
);
}
Ok(vec![app_path])
}
/// Package a signed iOS `.app` into an App Store distributable `.ipa`.
pub(crate) async fn bundle_ios_ipa(&self, bundles: &[Bundle]) -> Result<IosBundled> {
validate_ios_ipa_target(&self.target(), self.binary_arch())?;
let (app_paths, synthesized_app_paths) = if let Some(app_bundle) = bundles
.iter()
.find(|bundle| bundle.package_type == PackageType::IosApp)
{
(app_bundle.bundle_paths.clone(), Vec::new())
} else {
let paths = self.bundle_ios_app().await?;
(paths.clone(), paths)
};
if app_paths.is_empty() {
bail!("No iOS .app bundle found to package into an .ipa");
}
let app_path = &app_paths[0];
if !app_path.exists() {
bail!(
"iOS .app bundle does not exist at expected path: {}",
app_path.display()
);
}
verify_codesigned_app(app_path).await?;
let output_dir = self.project_out_directory().join("bundle").join("ipa");
std::fs::create_dir_all(&output_dir)?;
let ipa_name = format!(
"{}_{}_{}.ipa",
self.product_name(),
self.version_string(),
self.binary_arch()
);
let output_path = output_dir.join(ipa_name);
tracing::info!("Creating iOS IPA at {}", output_path.display());
let staging_dir =
tempfile::tempdir().context("Failed to create temp dir for IPA staging")?;
let payload_dir = staging_dir.path().join("Payload");
std::fs::create_dir_all(&payload_dir)?;
let staged_app = payload_dir.join(
app_path
.file_name()
.context("iOS .app bundle is missing a file name")?,
);
copy_dir_recursive(app_path, &staged_app)?;
if output_path.exists() {
std::fs::remove_file(&output_path)?;
}
zip_dir_recursive(staging_dir.path(), &output_path)?;
tracing::info!("IPA created at {}", output_path.display());
Ok(IosBundled {
ipa: vec![output_path],
app: synthesized_app_paths,
})
}
}
fn validate_ios_ipa_target(target: &str, arch: Arch) -> Result<()> {
let is_device = arch == Arch::AArch64 && !target.contains("sim");
if !is_device {
bail!(
"IPA packaging requires a physical-device iOS target. Resolved target was `{target}`."
);
}
Ok(())
}
async fn verify_codesigned_app(app_path: &Path) -> Result<()> {
verify_codesigned_app_with(Path::new("codesign"), app_path).await
}
async fn verify_codesigned_app_with(codesign: &Path, app_path: &Path) -> Result<()> {
let output = Command::new(codesign)
.args(["--verify", "--deep", "--strict"])
.arg(app_path)
.output()
.await
.with_context(|| format!("Failed to run `{}`", codesign.display()))?;
if !output.status.success() {
bail!(
"iOS .app bundle must be codesigned before creating an .ipa: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
}
Ok(())
}
File diff suppressed because it is too large. Load diff
+981
View File
@@ -0,0 +1,981 @@
use crate::bundler::{copy_dir_recursive, AppCategory, Bundle, BundleContext};
use crate::{MacOsSettings, PackageType};
use anyhow::{bail, Context, Result};
use image::{DynamicImage, ImageReader};
use std::fs::{self, File};
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use std::process::{Command as StdCommand, Stdio};
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
impl BundleContext<'_> {
/// Create the final macOS `.app` bundle and apply Apple-specific metadata.
///
/// This is the primary macOS bundling routine. It takes the executable produced by
/// the Dioxus build pipeline and assembles the canonical app bundle structure in
/// `project_out_directory()/macos/<BundleName>.app`.
///
/// The method performs the following steps:
/// 1. Resolve macOS settings and choose the bundle/display name.
/// 2. Remove any existing output bundle to keep the result deterministic.
/// 3. Create the `Contents/`, `Contents/MacOS`, `Contents/Resources`, and
/// optionally `Contents/Frameworks` directories.
/// 4. Copy the main executable into `Contents/MacOS/` and mark it executable.
/// 5. Build or copy an `.icns` file into `Contents/Resources/`.
/// 6. Copy configured resources and sidecar binaries into the bundle.
/// 7. Copy configured frameworks and arbitrary extra files from
/// `MacOsSettings::files`.
/// 8. Generate an `Info.plist` from bundle metadata, unless a custom plist path
/// was configured and exists.
/// 9. Write the legacy `PkgInfo` file.
/// 10. If signing is configured, sign frameworks first, then the main binary, and
/// finally the enclosing `.app`.
/// 11. If notarization credentials are available, zip the `.app`, submit it to
/// Apple's notary service, staple the result, and delete the temporary zip.
///
/// Contributor notes:
/// - The build system guarantees the executable exists, but this method is
/// responsible for the final `.app` on-disk shape.
/// - Framework paths may be absolute or relative to the crate directory. Missing
/// paths are tolerated because some entries may refer to system frameworks.
/// - Lack of signing or notarization credentials is not fatal; an unsigned `.app`
/// is still returned as a valid bundle artifact.
pub(crate) async fn bundle_macos_app(&self) -> Result<Vec<PathBuf>> {
let product_name = self.product_name();
let macos_settings = self.macos();
let bundle_name = macos_settings
.bundle_name
.as_deref()
.unwrap_or(&product_name);
let output_dir = self.project_out_directory().join("macos");
let app_dir = output_dir.join(format!("{bundle_name}.app"));
tracing::info!("Creating macOS .app bundle at {}", app_dir.display());
if app_dir.exists() {
fs::remove_dir_all(&app_dir)
.with_context(|| format!("Failed to clean existing .app: {}", app_dir.display()))?;
}
let contents_dir = app_dir.join("Contents");
let macos_dir = contents_dir.join("MacOS");
let resources_dir = contents_dir.join("Resources");
let frameworks_dir = contents_dir.join("Frameworks");
fs::create_dir_all(&macos_dir)?;
fs::create_dir_all(&resources_dir)?;
let binary_src = self.main_binary_path();
let binary_dest = macos_dir.join(self.main_binary_name());
tracing::debug!(
"Copying binary: {} -> {}",
binary_src.display(),
binary_dest.display()
);
fs::copy(&binary_src, &binary_dest)
.with_context(|| format!("Failed to copy main binary to {}", binary_dest.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&binary_dest, fs::Permissions::from_mode(0o755))?;
}
let icon_path = self.create_macos_icns_file(&resources_dir)?;
let icon_filename = icon_path
.as_ref()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().to_string());
self.copy_resources(&resources_dir)?;
let _external_bins = self.copy_external_binaries(&macos_dir)?;
if let Some(frameworks) = &macos_settings.frameworks {
if !frameworks.is_empty() {
fs::create_dir_all(&frameworks_dir)?;
for framework in frameworks {
let framework_path = PathBuf::from(framework);
if !framework_path.exists() {
let resolved = self.crate_dir().join(&framework_path);
if resolved.exists() {
copy_framework(&resolved, &frameworks_dir)?;
} else {
tracing::debug!(
"Framework not found as file, assuming system framework: {}",
framework
);
}
} else {
copy_framework(&framework_path, &frameworks_dir)?;
}
}
}
}
for (bundle_path, source_path) in &macos_settings.files {
let dest = contents_dir.join(bundle_path);
let src = if source_path.is_relative() {
self.crate_dir().join(source_path)
} else {
source_path.clone()
};
if !src.exists() {
tracing::warn!(
"Custom file not found: {} (for bundle path {})",
src.display(),
bundle_path.display()
);
continue;
}
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
if src.is_dir() {
copy_dir_recursive(&src, &dest)?;
} else {
fs::copy(&src, &dest).with_context(|| {
format!(
"Failed to copy custom file {} -> {}",
src.display(),
dest.display()
)
})?;
}
}
let info_plist = self.create_macos_info_plist(&macos_settings, icon_filename.as_deref())?;
let plist_path = contents_dir.join("Info.plist");
if let Some(custom_plist_path) = &macos_settings.info_plist_path {
let custom_path = if custom_plist_path.is_relative() {
self.crate_dir().join(custom_plist_path)
} else {
custom_plist_path.clone()
};
if custom_path.exists() {
tracing::info!("Using custom Info.plist from {}", custom_path.display());
fs::copy(&custom_path, &plist_path)?;
} else {
tracing::warn!(
"Custom Info.plist not found at {}, generating default",
custom_path.display()
);
write_plist(&info_plist, &plist_path)?;
}
} else {
write_plist(&info_plist, &plist_path)?;
}
fs::write(contents_dir.join("PkgInfo"), "APPL????")?;
let signing_identity = setup_keychain(macos_settings.signing_identity.as_deref()).await?;
if let Some(identity) = &signing_identity {
tracing::info!("Signing .app bundle with identity: {}", identity.identity);
let mut sign_targets = Vec::new();
if frameworks_dir.exists() {
for entry in fs::read_dir(&frameworks_dir)? {
let entry = entry?;
sign_targets.push(SignTarget { path: entry.path() });
}
}
sign_targets.push(SignTarget {
path: binary_dest.clone(),
});
sign_targets.push(SignTarget {
path: app_dir.clone(),
});
sign_paths(identity, sign_targets, &macos_settings).await?;
let should_notarize =
std::env::var("APPLE_ID").is_ok() || std::env::var("APPLE_API_KEY").is_ok();
if should_notarize {
let zip_path = output_dir.join(format!("{bundle_name}.zip"));
tracing::info!("Creating zip for notarization: {}", zip_path.display());
let status = Command::new("ditto")
.args([
"-c",
"-k",
"--sequesterRsrc",
"--keepParent",
&app_dir.display().to_string(),
&zip_path.display().to_string(),
])
.status()
.await
.context("Failed to run ditto for zip creation")?;
if !status.success() {
bail!("ditto failed to create zip for notarization");
}
match notarize(&zip_path).await {
Ok(()) => {
let _ = fs::remove_file(&zip_path);
}
Err(e) => {
let _ = fs::remove_file(&zip_path);
return Err(e.context("Notarization failed"));
}
}
}
} else {
tracing::debug!("No signing identity found; skipping code signing");
}
Ok(vec![app_dir])
}
/// Package the macOS application bundle into a distributable `.dmg` disk image.
///
/// DMG generation depends on having a complete `.app` bundle first. If the
/// current bundling pass already produced one, this method reuses it. Otherwise it
/// calls [`BundleContext::bundle_macos_app`] internally and returns both the
/// resulting `.dmg` and the intermediate `.app` paths so the higher-level
/// orchestrator can decide whether the `.app` should be preserved or cleaned up.
///
/// The process is:
/// 1. Resolve the `.app` input, either from `bundles` or by building it now.
/// 2. Create a temporary staging directory containing the `.app` and an
/// `Applications` symlink for drag-and-drop installation.
/// 3. Invoke `hdiutil create -format UDZO` to build a compressed, read-only disk
/// image in `project_out_directory()/macos`.
/// 4. Optionally sign the generated `.dmg`.
/// 5. Optionally notarize and staple the `.dmg` if Apple credentials are present.
///
/// Only the final `.dmg` and, when synthesized as a prerequisite, the `.app` are
/// considered outputs. The temporary DMG staging directory is always discarded.
pub(crate) async fn bundle_macos_dmg(&self, bundles: &[Bundle]) -> Result<DmgBundled> {
let product_name = self.product_name();
let macos_settings = self.macos();
let bundle_name = macos_settings
.bundle_name
.as_deref()
.unwrap_or(&product_name);
let (app_paths, app_bundle_paths) = if let Some(app_bundle) = bundles
.iter()
.find(|b| b.package_type == PackageType::MacOsBundle)
{
(app_bundle.bundle_paths.clone(), Vec::new())
} else {
let paths = self.bundle_macos_app().await?;
(paths.clone(), paths)
};
if app_paths.is_empty() {
bail!("No .app bundle found to package into a DMG");
}
let app_path = &app_paths[0];
if !app_path.exists() {
bail!(
".app bundle does not exist at expected path: {}",
app_path.display()
);
}
let output_dir = self.project_out_directory().join("macos");
fs::create_dir_all(&output_dir)?;
let dmg_filename = format!(
"{}_{}_{}",
bundle_name,
self.version_string(),
self.binary_arch()
);
let dmg_path = output_dir.join(format!("{dmg_filename}.dmg"));
tracing::info!("Creating DMG at {}", dmg_path.display());
let staging_dir =
tempfile::tempdir().context("Failed to create temp dir for DMG staging")?;
let staging_path = staging_dir.path();
let staged_app = staging_path.join(app_path.file_name().unwrap());
copy_dir_recursive(app_path, &staged_app)?;
#[cfg(unix)]
{
std::os::unix::fs::symlink("/Applications", staging_path.join("Applications"))
.context("Failed to create /Applications symlink in DMG staging")?;
}
if dmg_path.exists() {
fs::remove_file(&dmg_path)?;
}
let status = Command::new("hdiutil")
.args([
"create",
"-volname",
bundle_name,
"-srcfolder",
&staging_path.display().to_string(),
"-ov",
"-format",
"UDZO",
&dmg_path.display().to_string(),
])
.status()
.await
.context("Failed to execute `hdiutil create`")?;
if !status.success() {
bail!("`hdiutil create` failed with exit code: {status}");
}
tracing::info!("DMG created at {}", dmg_path.display());
let signing_identity = setup_keychain(macos_settings.signing_identity.as_deref()).await?;
if let Some(identity) = &signing_identity {
tracing::info!("Signing DMG with identity: {}", identity.identity);
sign_paths(
identity,
vec![SignTarget {
path: dmg_path.clone(),
}],
&macos_settings,
)
.await?;
let should_notarize =
std::env::var("APPLE_ID").is_ok() || std::env::var("APPLE_API_KEY").is_ok();
if should_notarize {
notarize(&dmg_path).await?;
}
}
Ok(DmgBundled {
dmg: vec![dmg_path],
app: app_bundle_paths,
})
}
/// Create an ICNS file from the icon files configured in the bundle context.
fn create_macos_icns_file(&self, out_dir: &Path) -> Result<Option<PathBuf>> {
let icon_paths = self.icon_files()?;
if icon_paths.is_empty() {
return Ok(None);
}
let dest_path = out_dir.join(format!("{}.icns", self.product_name()));
for icon_path in &icon_paths {
if icon_path
.extension()
.map(|ext| ext.eq_ignore_ascii_case("icns"))
.unwrap_or(false)
{
tracing::info!("Copying existing .icns file: {}", icon_path.display());
std::fs::copy(icon_path, &dest_path).with_context(|| {
format!(
"Failed to copy .icns file from {} to {}",
icon_path.display(),
dest_path.display()
)
})?;
return Ok(Some(dest_path));
}
}
let mut family = icns::IconFamily::new();
for icon_path in &icon_paths {
let ext = icon_path
.extension()
.map(|e| e.to_string_lossy().to_lowercase())
.unwrap_or_default();
if ext != "png" {
tracing::debug!("Skipping non-PNG icon file: {}", icon_path.display());
continue;
}
let img = ImageReader::open(icon_path)
.with_context(|| format!("Failed to open icon image: {}", icon_path.display()))?
.decode()
.with_context(|| format!("Failed to decode icon image: {}", icon_path.display()))?;
add_image_to_family(&mut family, &img)?;
}
if family.is_empty() {
tracing::warn!("No valid icon images found; skipping .icns generation");
return Ok(None);
}
let file = File::create(&dest_path)
.with_context(|| format!("Failed to create {}", dest_path.display()))?;
let writer = BufWriter::new(file);
family
.write(writer)
.with_context(|| format!("Failed to write .icns to {}", dest_path.display()))?;
tracing::info!("Generated .icns at {}", dest_path.display());
Ok(Some(dest_path))
}
/// Build a `plist::Dictionary` for Info.plist.
fn create_macos_info_plist(
&self,
macos_settings: &MacOsSettings,
icon_filename: Option<&str>,
) -> Result<plist::Dictionary> {
let mut dict = plist::Dictionary::new();
let product_name = self.product_name();
let bundle_name = macos_settings
.bundle_name
.as_deref()
.unwrap_or(&product_name);
dict.insert(
"CFBundleDevelopmentRegion".into(),
plist::Value::String("English".into()),
);
dict.insert(
"CFBundleDisplayName".into(),
plist::Value::String(bundle_name.to_string()),
);
dict.insert(
"CFBundleExecutable".into(),
plist::Value::String(self.main_binary_name().to_string()),
);
if let Some(icon) = icon_filename {
dict.insert(
"CFBundleIconFile".into(),
plist::Value::String(icon.to_string()),
);
}
dict.insert(
"CFBundleIdentifier".into(),
plist::Value::String(self.bundle_identifier()),
);
dict.insert(
"CFBundleInfoDictionaryVersion".into(),
plist::Value::String("6.0".into()),
);
dict.insert(
"CFBundleName".into(),
plist::Value::String(bundle_name.to_string()),
);
dict.insert(
"CFBundlePackageType".into(),
plist::Value::String("APPL".into()),
);
let version = self.version_string();
let bundle_version = macos_settings.bundle_version.as_deref().unwrap_or(&version);
dict.insert(
"CFBundleShortVersionString".into(),
plist::Value::String(version.clone()),
);
dict.insert(
"CFBundleVersion".into(),
plist::Value::String(bundle_version.to_string()),
);
let min_version = macos_settings
.minimum_system_version
.as_deref()
.unwrap_or("10.13");
dict.insert(
"LSMinimumSystemVersion".into(),
plist::Value::String(min_version.to_string()),
);
if let Some(category_str) = self.app_category() {
if let Ok(category) = category_str.parse::<AppCategory>() {
dict.insert(
"LSApplicationCategoryType".into(),
plist::Value::String(category.macos_application_category_type().to_string()),
);
}
}
if let Some(copyright) = self.copyright_string() {
dict.insert(
"NSHumanReadableCopyright".into(),
plist::Value::String(copyright.to_string()),
);
}
dict.insert(
"NSHighResolutionCapable".into(),
plist::Value::Boolean(true),
);
if let Some(domain) = &macos_settings.exception_domain {
let mut ats_dict = plist::Dictionary::new();
let mut exception_dict = plist::Dictionary::new();
let mut domain_dict = plist::Dictionary::new();
domain_dict.insert(
"NSExceptionAllowsInsecureHTTPLoads".into(),
plist::Value::Boolean(true),
);
domain_dict.insert("NSIncludesSubdomains".into(), plist::Value::Boolean(true));
exception_dict.insert(domain.clone(), plist::Value::Dictionary(domain_dict));
ats_dict.insert(
"NSExceptionDomains".into(),
plist::Value::Dictionary(exception_dict),
);
dict.insert(
"NSAppTransportSecurity".into(),
plist::Value::Dictionary(ats_dict),
);
}
if let Some(provider) = &macos_settings.provider_short_name {
dict.insert(
"ITSAppUsesNonExemptEncryption".into(),
plist::Value::Boolean(false),
);
let _ = provider;
}
Ok(dict)
}
}
/// Add all appropriate size variants of an image to the ICNS family.
fn add_image_to_family(family: &mut icns::IconFamily, img: &DynamicImage) -> Result<()> {
// The icon sizes (in points) we generate for the .icns file, along with their
// densities. macOS expects both 1x and 2x variants.
const ICON_SIZES: &[(u32, u32, u32)] = &[
(16, 16, 1),
(16, 16, 2),
(32, 32, 1),
(32, 32, 2),
(64, 64, 1),
(64, 64, 2),
(128, 128, 1),
(128, 128, 2),
(256, 256, 1),
(256, 256, 2),
(512, 512, 1),
(512, 512, 2),
];
for &(width, height, density) in ICON_SIZES {
let pixel_width = width * density;
let pixel_height = height * density;
let icon_type =
match icns::IconType::from_pixel_size_and_density(pixel_width, pixel_height, density) {
Some(t) => t,
None => continue,
};
if family.has_icon_with_type(icon_type) {
continue;
}
let resized = img.resize_exact(
pixel_width,
pixel_height,
image::imageops::FilterType::Lanczos3,
);
let rgba = resized.to_rgba8();
let icns_image = icns::Image::from_data(
icns::PixelFormat::RGBA,
pixel_width,
pixel_height,
rgba.into_raw(),
)
.with_context(|| {
format!("Failed to create icns::Image for {pixel_width}x{pixel_height}@{density}x")
})?;
family
.add_icon_with_type(&icns_image, icon_type)
.with_context(|| format!("Failed to add icon type {icon_type:?} to ICNS family"))?;
}
Ok(())
}
/// Write a plist dictionary to a file.
fn write_plist(dict: &plist::Dictionary, path: &Path) -> Result<()> {
plist::Value::Dictionary(dict.clone())
.to_file_xml(path)
.with_context(|| format!("Failed to write Info.plist to {}", path.display()))
}
/// Copy a framework (directory or .dylib) to the Frameworks directory.
fn copy_framework(src: &Path, frameworks_dir: &Path) -> Result<()> {
let dest = frameworks_dir.join(src.file_name().context("Framework path has no filename")?);
tracing::debug!("Copying framework: {} -> {}", src.display(), dest.display());
if src.is_dir() {
copy_dir_recursive(src, &dest)?;
} else {
fs::copy(src, &dest)?;
}
Ok(())
}
/// Set up the signing identity.
async fn setup_keychain(identity: Option<&str>) -> Result<Option<SigningIdentity>> {
let certificate_encoded = std::env::var("APPLE_CERTIFICATE").ok();
let certificate_password = std::env::var("APPLE_CERTIFICATE_PASSWORD")
.ok()
.unwrap_or_default();
if let Some(cert_base64) = certificate_encoded {
tracing::info!("Setting up temporary keychain for code signing (CI mode)");
let keychain = setup_temp_keychain(&cert_base64, &certificate_password).await?;
let identity_name = find_identity_in_keychain(&keychain.path).await?;
return Ok(Some(SigningIdentity {
identity: identity_name,
temp_keychain: Some(keychain),
}));
}
if let Some(id) = identity {
if !id.is_empty() {
return Ok(Some(SigningIdentity {
identity: id.to_string(),
temp_keychain: None,
}));
}
}
Ok(None)
}
/// Set up a temporary keychain and import the certificate into it.
async fn setup_temp_keychain(cert_base64: &str, password: &str) -> Result<TempKeychain> {
use std::io::Write;
let keychain_password = "dioxus-bundle-keychain";
let keychain_path = std::env::temp_dir().join("dioxus-signing.keychain-db");
let cert_data = base64_decode(cert_base64)
.await
.context("Failed to decode APPLE_CERTIFICATE from base64")?;
let cert_file = std::env::temp_dir().join("dioxus-signing-cert.p12");
let mut f = std::fs::File::create(&cert_file)?;
f.write_all(&cert_data)?;
drop(f);
let _ = Command::new("security")
.args(["delete-keychain", &keychain_path.display().to_string()])
.output()
.await;
let mut create_keychain_cmd = Command::new("security");
create_keychain_cmd.args([
"create-keychain",
"-p",
keychain_password,
&keychain_path.display().to_string(),
]);
run_command(&mut create_keychain_cmd, "create-keychain").await?;
let mut import_cmd = Command::new("security");
import_cmd.args([
"import",
&cert_file.display().to_string(),
"-k",
&keychain_path.display().to_string(),
"-P",
password,
"-T",
"/usr/bin/codesign",
"-T",
"/usr/bin/security",
]);
run_command(&mut import_cmd, "import certificate").await?;
let output = Command::new("security")
.args(["list-keychains", "-d", "user"])
.output()
.await
.context("Failed to list keychains")?;
let current_keychains = String::from_utf8_lossy(&output.stdout);
let mut keychains: Vec<String> = current_keychains
.lines()
.map(|l| l.trim().trim_matches('"').to_string())
.filter(|l| !l.is_empty())
.collect();
keychains.insert(0, keychain_path.display().to_string());
let mut cmd = Command::new("security");
cmd.args(["list-keychains", "-d", "user", "-s"]);
for kc in &keychains {
cmd.arg(kc);
}
run_command(&mut cmd, "list-keychains -s").await?;
let mut unlock_cmd = Command::new("security");
unlock_cmd.args([
"unlock-keychain",
"-p",
keychain_password,
&keychain_path.display().to_string(),
]);
run_command(&mut unlock_cmd, "unlock-keychain").await?;
let mut set_partition_cmd = Command::new("security");
set_partition_cmd.args([
"set-key-partition-list",
"-S",
"apple-tool:,apple:",
"-s",
"-k",
keychain_password,
&keychain_path.display().to_string(),
]);
run_command(&mut set_partition_cmd, "set-key-partition-list").await?;
let _ = std::fs::remove_file(&cert_file);
Ok(TempKeychain {
path: keychain_path,
password: keychain_password.to_string(),
})
}
/// Find the signing identity in a keychain using `security find-identity`.
async fn find_identity_in_keychain(keychain_path: &Path) -> Result<String> {
let output = Command::new("security")
.args([
"find-identity",
"-v",
"-p",
"codesigning",
&keychain_path.display().to_string(),
])
.output()
.await
.context("Failed to run `security find-identity`")?;
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let line = line.trim();
if line.starts_with("1)")
|| line.contains("Developer ID")
|| line.contains("Apple Development")
{
if let Some(start) = line.find('"') {
if let Some(end) = line.rfind('"') {
if end > start {
return Ok(line[start + 1..end].to_string());
}
}
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 && parts[1].len() == 40 {
return Ok(parts[1].to_string());
}
}
}
bail!(
"No valid signing identity found in keychain {}.\nOutput: {}",
keychain_path.display(),
stdout
)
}
/// Sign a list of paths with the given identity.
async fn sign_paths(
identity: &SigningIdentity,
targets: Vec<SignTarget>,
settings: &MacOsSettings,
) -> Result<()> {
for target in &targets {
sign_path(identity, target, settings).await?;
}
Ok(())
}
/// Sign a single path with `codesign`.
async fn sign_path(
identity: &SigningIdentity,
target: &SignTarget,
settings: &MacOsSettings,
) -> Result<()> {
tracing::info!("Signing: {}", target.path.display());
let mut cmd = Command::new("codesign");
cmd.args(["--force", "--sign", &identity.identity]);
if settings.hardened_runtime {
cmd.arg("--options");
cmd.arg("runtime");
}
if target
.path
.extension()
.map(|e| e == "app" || e == "framework")
.unwrap_or(false)
{
cmd.arg("--deep");
}
if let Some(entitlements) = &settings.entitlements {
cmd.args(["--entitlements", entitlements]);
}
if let Some(keychain) = &identity.temp_keychain {
cmd.args(["--keychain", &keychain.path.display().to_string()]);
}
cmd.arg(&target.path);
run_command(&mut cmd, &format!("codesign {}", target.path.display())).await?;
Ok(())
}
/// Notarize a .app or .dmg with Apple's notary service.
async fn notarize(app_path: &Path) -> Result<()> {
let apple_id = std::env::var("APPLE_ID").ok();
let apple_password = std::env::var("APPLE_PASSWORD").ok();
let apple_team_id = std::env::var("APPLE_TEAM_ID").ok();
let api_key = std::env::var("APPLE_API_KEY").ok();
let api_issuer = std::env::var("APPLE_API_ISSUER").ok();
let api_key_path = std::env::var("APPLE_API_KEY_PATH").ok();
let mut cmd = Command::new("xcrun");
cmd.args(["notarytool", "submit"]);
cmd.arg(app_path);
if let (Some(key), Some(issuer), Some(key_path)) = (&api_key, &api_issuer, &api_key_path) {
cmd.args(["--key", key_path]);
cmd.args(["--key-id", key]);
cmd.args(["--issuer", issuer]);
} else if let (Some(id), Some(pwd), Some(team)) = (&apple_id, &apple_password, &apple_team_id) {
cmd.args(["--apple-id", id]);
cmd.args(["--password", pwd]);
cmd.args(["--team-id", team]);
} else {
bail!(
"Notarization requires either:\n\
- APPLE_ID, APPLE_PASSWORD, and APPLE_TEAM_ID env vars, or\n\
- APPLE_API_KEY, APPLE_API_ISSUER, and APPLE_API_KEY_PATH env vars"
);
}
cmd.arg("--wait");
tracing::info!("Submitting {} for notarization...", app_path.display());
run_command(&mut cmd, "xcrun notarytool submit").await?;
tracing::info!("Stapling notarization ticket...");
let mut staple_cmd = Command::new("xcrun");
staple_cmd.args(["stapler", "staple"]).arg(app_path);
run_command(&mut staple_cmd, "xcrun stapler staple").await?;
tracing::info!("Notarization complete for {}", app_path.display());
Ok(())
}
/// Helper to run a command and return a nice error on failure.
async fn run_command(cmd: &mut Command, description: &str) -> Result<()> {
tracing::debug!("Running: {:?}", cmd);
let status = cmd
.status()
.await
.with_context(|| format!("Failed to execute `{description}`"))?;
if !status.success() {
bail!("`{description}` failed with exit code: {}", status);
}
Ok(())
}
/// Decode base64 (standard or URL-safe).
async fn base64_decode(input: &str) -> Result<Vec<u8>> {
let mut child = Command::new("base64")
.args(["--decode"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("Failed to decode base64 certificate")?;
if let Some(mut stdin) = child.stdin.take() {
stdin
.write_all(input.as_bytes())
.await
.context("Failed writing base64 certificate to decoder stdin")?;
}
let output = child
.wait_with_output()
.await
.context("Failed to decode base64 certificate")?;
if !output.status.success() {
bail!(
"base64 --decode failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(output.stdout)
}
/// The result of DMG bundling, which may include both the `.dmg` and `.app` outputs.
pub(crate) struct DmgBundled {
/// Paths to the generated `.dmg` file(s).
pub dmg: Vec<PathBuf>,
/// Paths to the generated `.app` bundle(s) (if the `.app` was built as a dependency).
pub app: Vec<PathBuf>,
}
/// A target to be code-signed.
struct SignTarget {
path: PathBuf,
}
/// A code signing identity, optionally backed by a temporary keychain.
struct SigningIdentity {
/// The identity string passed to `codesign --sign`.
/// This is either a team/certificate name or a SHA-1 hash.
identity: String,
/// If we created a temporary keychain to import a certificate,
/// this holds its path so we can clean it up later.
temp_keychain: Option<TempKeychain>,
}
/// A temporary keychain created for CI certificate import.
#[allow(dead_code)]
struct TempKeychain {
path: PathBuf,
password: String,
}
impl Drop for TempKeychain {
fn drop(&mut self) {
tracing::debug!("Cleaning up temporary keychain: {}", self.path.display());
let _ = StdCommand::new("security")
.args(["delete-keychain", &self.path.display().to_string()])
.status();
}
}
+892
View File
@@ -0,0 +1,892 @@
mod ios;
mod linux;
mod macos;
mod tools;
mod updater;
mod windows;
use crate::PackageType;
use crate::{BuildRequest, DebianSettings, MacOsSettings, WindowsSettings};
use anyhow::Context;
use anyhow::Result;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tools::ResolvedTools;
/// A completed bundle with its output paths.
#[derive(Debug)]
pub(crate) struct Bundle {
pub package_type: PackageType,
pub bundle_paths: Vec<PathBuf>,
}
/// BundleContext wraps a BuildRequest and provides the settings API
/// that bundle format modules need. This is the adapter between
/// Dioxus build infrastructure and the individual bundler implementations.
pub(crate) struct BundleContext<'a> {
pub(crate) build: &'a BuildRequest,
pub(crate) package_types: Vec<PackageType>,
/// Pre-computed resource map: source path -> target path in bundle
pub(crate) resources_map: HashMap<String, String>,
/// Pre-resolved tool paths (NSIS, WiX, linuxdeploy, WebView2).
pub(crate) tools: ResolvedTools,
}
impl<'a> BundleContext<'a> {
/// Create a new BundleContext from a BuildRequest and optional package types.
pub(crate) async fn new(
build: &'a BuildRequest,
package_types: &Option<Vec<PackageType>>,
) -> Result<Self> {
let package_types = package_types.clone().unwrap_or_default();
// Build the resources map from assets + config resources
let mut resources_map = HashMap::new();
let asset_dir = build.asset_dir();
if asset_dir.exists() {
for entry in walkdir::WalkDir::new(&asset_dir) {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let old = path
.canonicalize()
.with_context(|| format!("Failed to canonicalize {entry:?}"))?;
let new =
PathBuf::from("assets").join(path.strip_prefix(&asset_dir).unwrap_or(path));
resources_map.insert(old.display().to_string(), new.display().to_string());
}
}
}
// Merge in any custom resources from config
if let Some(resources) = &build.config.bundle.resources {
for resource_path in resources {
resources_map.insert(resource_path.clone(), String::new());
}
}
let tools_dir = dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from(".cache"))
.join("dioxus");
let _ = std::fs::create_dir_all(&tools_dir);
let arch = {
let target = build.triple.to_string();
if target.starts_with("x86_64") {
Arch::X86_64
} else if target.starts_with('i') {
Arch::X86
} else if target.starts_with("arm") && target.ends_with("hf") {
Arch::Armhf
} else if target.starts_with("arm") {
Arch::Armel
} else if target.starts_with("aarch64") {
Arch::AArch64
} else if target.starts_with("riscv64") {
Arch::Riscv64
} else if target.starts_with("universal") {
Arch::Universal
} else if cfg!(target_arch = "x86_64") {
Arch::X86_64
} else if cfg!(target_arch = "aarch64") {
Arch::AArch64
} else {
Arch::X86_64
}
};
let windows_settings = build.config.bundle.windows.clone().unwrap_or_default();
let tools =
tools::resolve_tools(&tools_dir, &package_types, &windows_settings, arch).await?;
Ok(Self {
build,
package_types,
resources_map,
tools,
})
}
/// Bundle the project for every configured package type, in dependency order.
///
/// This is the orchestration entrypoint for native packaging. It owns the
/// sequencing rules between bundle formats, but delegates the actual format
/// implementation to the per-platform `bundle_*` methods on `BundleContext`.
///
/// The method performs the following high-level workflow:
/// 1. Read the resolved package types from the immutable context.
/// 2. Sort them so prerequisite artifacts are built before dependents.
/// In practice, this means raw distributable formats such as `.app`, `.deb`,
/// `.rpm`, `.AppImage`, `.msi`, `.exe`, `.apk`, and `.aab` run before
/// dependent archive formats like `.ipa`/`.dmg`, and `Updater` always runs last.
/// 3. Dispatch to the top-level format-specific bundling method for each package
/// type and collect the artifact paths it returns.
/// 4. Reuse outputs where formats depend on each other. For example, DMG creation
/// may synthesize a `.app` bundle and return it alongside the `.dmg`.
/// 5. Remove intermediate macOS `.app` bundles when they were only needed as an
/// implementation detail of `.dmg` or updater generation.
///
/// The returned [`Bundle`] values are the canonical record of what was emitted by
/// the bundling pass. Later steps such as updater packaging rely on these paths
/// rather than rediscovering artifacts on disk.
pub(crate) async fn bundle_project(&self) -> Result<Vec<Bundle>> {
let mut package_types = self.package_types();
// Sort so dependencies come first (e.g. .app before .dmg)
package_types.sort_by_key(|ptype| match ptype {
PackageType::MacOsBundle
| PackageType::IosApp
| PackageType::WindowsMsi
| PackageType::Nsis
| PackageType::Deb
| PackageType::Rpm
| PackageType::AppImage
| PackageType::Apk
| PackageType::Aab => 0,
PackageType::Ipa | PackageType::Dmg => 1,
PackageType::Updater => 2,
});
let mut bundles = Vec::<Bundle>::new();
for package_type in &package_types {
// Skip if already built (e.g. DMG already built .app)
if bundles.iter().any(|b| b.package_type == *package_type) {
continue;
}
let bundle_paths = match package_type {
PackageType::MacOsBundle => self.bundle_macos_app().await?,
PackageType::IosApp => self.bundle_ios_app().await?,
PackageType::Ipa => self.bundle_ios_ipa(&bundles).await?.ipa,
PackageType::Dmg => {
let bundled = self.bundle_macos_dmg(&bundles).await?;
if !bundled.app.is_empty() {
bundles.push(Bundle {
package_type: PackageType::MacOsBundle,
bundle_paths: bundled.app,
});
}
bundled.dmg
}
PackageType::Deb => self.bundle_linux_deb().await?,
PackageType::Rpm => self.bundle_linux_rpm().await?,
PackageType::AppImage => self.bundle_linux_appimage().await?,
PackageType::WindowsMsi => self.bundle_windows_msi().await?,
PackageType::Nsis => self.bundle_windows_nsis().await?,
PackageType::Updater => self.bundle_updater(&bundles).await?,
PackageType::Apk | PackageType::Aab => self.bundle_android(*package_type).await?,
};
bundles.push(Bundle {
package_type: *package_type,
bundle_paths,
});
}
// On macOS, clean up .app if only building dmg or updater
if !package_types.contains(&PackageType::MacOsBundle) {
if let Some(idx) = bundles
.iter()
.position(|b| b.package_type == PackageType::MacOsBundle)
{
let app_bundle = bundles.remove(idx);
for path in &app_bundle.bundle_paths {
tracing::info!("Cleaning up intermediate .app: {}", path.display());
if path.is_dir() {
let _ = std::fs::remove_dir_all(path);
} else {
let _ = std::fs::remove_file(path);
}
}
}
}
for bundle in &bundles {
for path in &bundle.bundle_paths {
tracing::info!("Bundled: {}", path.display());
}
}
Ok(bundles)
}
/// The package types to bundle.
pub(crate) fn package_types(&self) -> Vec<PackageType> {
self.package_types.clone()
}
/// The product name (PascalCase).
pub(crate) fn product_name(&self) -> String {
self.build.bundled_app_name()
}
/// The bundle identifier (e.g. "com.example.app").
pub(crate) fn bundle_identifier(&self) -> String {
self.build.bundle_identifier()
}
/// The publisher name.
pub(crate) fn publisher(&self) -> Option<&str> {
self.build.config.bundle.publisher.as_deref()
}
/// The version string from Cargo.toml.
pub(crate) fn version_string(&self) -> String {
self.build.package().version.to_string()
}
/// The short description.
pub(crate) fn short_description(&self) -> String {
self.build
.config
.bundle
.short_description
.clone()
.or_else(|| self.build.package().description.clone())
.unwrap_or_default()
}
/// The long description.
pub(crate) fn long_description(&self) -> Option<&str> {
self.build.config.bundle.long_description.as_deref()
}
/// The copyright string.
pub(crate) fn copyright_string(&self) -> Option<&str> {
self.build.config.bundle.copyright.as_deref()
}
/// The authors list.
pub(crate) fn authors(&self) -> &[String] {
&self.build.package().authors
}
/// Authors as a comma-separated string.
pub(crate) fn authors_comma_separated(&self) -> Option<String> {
let names = self.authors();
if names.is_empty() {
None
} else {
Some(names.join(", "))
}
}
/// The homepage URL.
pub(crate) fn homepage_url(&self) -> Option<String> {
self.build
.package()
.homepage
.as_ref()
.filter(|hp| !hp.is_empty())
.cloned()
}
/// The license string.
pub(crate) fn license(&self) -> Option<&str> {
self.build.package().license.as_deref()
}
/// The app category string.
pub(crate) fn app_category(&self) -> Option<&str> {
self.build.config.bundle.category.as_deref()
}
/// The main binary name (without extension).
pub(crate) fn main_binary_name(&self) -> &str {
self.build.executable_name()
}
/// The path to the main binary.
pub(crate) fn main_binary_path(&self) -> PathBuf {
self.build.main_exe()
}
/// The output directory for bundles.
pub(crate) fn project_out_directory(&self) -> PathBuf {
self.build.bundle_dir(self.build.bundle)
}
/// The target triple string.
pub(crate) fn target(&self) -> String {
self.build.triple.to_string()
}
/// The binary architecture.
pub(crate) fn binary_arch(&self) -> Arch {
let target = self.target();
if target.starts_with("x86_64") {
Arch::X86_64
} else if target.starts_with('i') {
Arch::X86
} else if target.starts_with("arm") && target.ends_with("hf") {
Arch::Armhf
} else if target.starts_with("arm") {
Arch::Armel
} else if target.starts_with("aarch64") {
Arch::AArch64
} else if target.starts_with("riscv64") {
Arch::Riscv64
} else if target.starts_with("universal") {
Arch::Universal
} else {
// Default to the host arch
if cfg!(target_arch = "x86_64") {
Arch::X86_64
} else if cfg!(target_arch = "aarch64") {
Arch::AArch64
} else {
Arch::X86_64
}
}
}
/// Icon file paths from config, canonicalized relative to the crate dir.
pub(crate) fn icon_files(&self) -> Result<Vec<PathBuf>> {
let mut icons = Vec::new();
if let Some(icon_paths) = &self.build.config.bundle.icon {
for icon in icon_paths {
let icon_path = self
.build
.crate_dir()
.join(icon)
.canonicalize()
.with_context(|| format!("Failed to canonicalize icon path: {icon}"))?;
icons.push(icon_path);
}
}
Ok(icons)
}
/// Copy resources to the given path.
pub(crate) fn copy_resources(&self, dest: &Path) -> Result<()> {
for (src, target) in &self.resources_map {
let src_path = PathBuf::from(src);
if !src_path.exists() {
tracing::warn!("Resource not found: {src}");
continue;
}
let dest_path = if target.is_empty() {
dest.join(src_path.file_name().unwrap_or_default())
} else {
dest.join(target)
};
if let Some(parent) = dest_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(&src_path, &dest_path).with_context(|| {
format!("Failed to copy resource {src} -> {}", dest_path.display())
})?;
}
Ok(())
}
/// Copy external binaries to the given path.
pub(crate) fn copy_external_binaries(&self, dest: &Path) -> Result<Vec<PathBuf>> {
let mut paths = Vec::new();
if let Some(bins) = &self.build.config.bundle.external_bin {
let target = self.target();
for bin in bins {
let src = PathBuf::from(format!("{bin}-{target}"));
if src.exists() {
let dest_name = src
.file_name()
.unwrap()
.to_string_lossy()
.replace(&format!("-{target}"), "");
let dest_path = dest.join(dest_name);
std::fs::copy(&src, &dest_path)?;
paths.push(dest_path);
}
}
}
Ok(paths)
}
/// The crate directory (where Cargo.toml is).
pub(crate) fn crate_dir(&self) -> PathBuf {
self.build.crate_dir()
}
/// Debian settings from config.
pub(crate) fn deb(&self) -> DebianSettings {
self.build.config.bundle.deb.clone().unwrap_or_default()
}
/// macOS settings from config.
pub(crate) fn macos(&self) -> MacOsSettings {
self.build.config.bundle.macos.clone().unwrap_or_default()
}
/// Windows settings from config.
pub(crate) fn windows(&self) -> WindowsSettings {
self.build.config.bundle.windows.clone().unwrap_or_default()
}
}
/// The architecture of the target binary.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Arch {
X86_64,
X86,
AArch64,
Armhf,
Armel,
Riscv64,
Universal,
}
impl Arch {
pub(crate) fn as_str(&self) -> &'static str {
match self {
Arch::X86_64 => "x86_64",
Arch::X86 => "x86",
Arch::AArch64 => "aarch64",
Arch::Armhf => "armhf",
Arch::Armel => "armel",
Arch::Riscv64 => "riscv64",
Arch::Universal => "universal",
}
}
pub(crate) fn deb_arch(&self) -> &'static str {
match self {
Arch::X86_64 => "amd64",
Arch::X86 => "i386",
Arch::AArch64 => "arm64",
Arch::Armhf => "armhf",
Arch::Armel => "armel",
Arch::Riscv64 => "riscv64",
Arch::Universal => "all",
}
}
pub(crate) fn rpm_arch(&self) -> &'static str {
match self {
Arch::X86_64 => "x86_64",
Arch::X86 => "i686",
Arch::AArch64 => "aarch64",
Arch::Armhf => "armv7hl",
Arch::Armel => "armv6l",
Arch::Riscv64 => "riscv64",
Arch::Universal => "noarch",
}
}
pub(crate) fn appimage_arch(&self) -> &'static str {
match self {
Arch::X86_64 => "x86_64",
Arch::X86 => "i386",
Arch::AArch64 => "aarch64",
Arch::Armhf => "armhf",
Arch::Armel => "armel",
Arch::Riscv64 => "riscv64",
Arch::Universal => "x86_64",
}
}
pub(crate) fn windows_arch(&self) -> &'static str {
match self {
Arch::X86_64 => "x64",
Arch::X86 => "x86",
Arch::AArch64 => "arm64",
_ => "x64",
}
}
pub(crate) fn wix_arch(&self) -> &'static str {
match self {
Arch::X86_64 => "x64",
Arch::X86 => "x86",
Arch::AArch64 => "arm64",
_ => "x64",
}
}
pub(crate) fn wix_program_files_folder(&self) -> &'static str {
match self {
Arch::X86 => "ProgramFilesFolder",
Arch::X86_64 | Arch::AArch64 => "ProgramFiles64Folder",
_ => "ProgramFiles64Folder",
}
}
pub(crate) fn linuxdeploy_arch(&self) -> &'static str {
match self {
Arch::X86_64 => "x86_64",
Arch::X86 => "i386",
Arch::AArch64 => "aarch64",
Arch::Armhf | Arch::Armel => "armhf",
Arch::Riscv64 | Arch::Universal => "x86_64",
}
}
}
impl std::fmt::Display for Arch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
use std::str::FromStr;
/// The possible app categories.
/// Corresponds to `LSApplicationCategoryType` on macOS and the GNOME desktop categories on Debian.
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum AppCategory {
Business,
DeveloperTool,
Education,
Entertainment,
Finance,
Game,
ActionGame,
AdventureGame,
ArcadeGame,
BoardGame,
CardGame,
CasinoGame,
DiceGame,
EducationalGame,
FamilyGame,
KidsGame,
MusicGame,
PuzzleGame,
RacingGame,
RolePlayingGame,
SimulationGame,
SportsGame,
StrategyGame,
TriviaGame,
WordGame,
GraphicsAndDesign,
HealthcareAndFitness,
Lifestyle,
Medical,
Music,
News,
Photography,
Productivity,
Reference,
SocialNetworking,
Sports,
Travel,
Utility,
Video,
Weather,
}
impl FromStr for AppCategory {
type Err = String;
fn from_str(input: &str) -> Result<AppCategory, Self::Err> {
const MACOS_APP_CATEGORY_PREFIX: &str = "public.app-category.";
let mut input = input.to_ascii_lowercase();
if input.starts_with(MACOS_APP_CATEGORY_PREFIX) {
input = input
.split_at(MACOS_APP_CATEGORY_PREFIX.len())
.1
.to_string();
}
input = input.replace(' ', "");
input = input.replace('-', "");
for &(string, category) in CATEGORY_STRINGS.iter() {
if input == string {
return Ok(category);
}
}
Err(format!("Unknown app category: {input}"))
}
}
impl AppCategory {
/// Map to closest set of Freedesktop categories.
pub(crate) fn freedesktop_categories(self) -> &'static str {
match &self {
AppCategory::Business => "Office;",
AppCategory::DeveloperTool => "Development;",
AppCategory::Education => "Education;",
AppCategory::Entertainment => "Network;",
AppCategory::Finance => "Office;Finance;",
AppCategory::Game => "Game;",
AppCategory::ActionGame => "Game;ActionGame;",
AppCategory::AdventureGame => "Game;AdventureGame;",
AppCategory::ArcadeGame => "Game;ArcadeGame;",
AppCategory::BoardGame => "Game;BoardGame;",
AppCategory::CardGame => "Game;CardGame;",
AppCategory::CasinoGame => "Game;",
AppCategory::DiceGame => "Game;",
AppCategory::EducationalGame => "Game;Education;",
AppCategory::FamilyGame => "Game;",
AppCategory::KidsGame => "Game;KidsGame;",
AppCategory::MusicGame => "Game;",
AppCategory::PuzzleGame => "Game;LogicGame;",
AppCategory::RacingGame => "Game;",
AppCategory::RolePlayingGame => "Game;RolePlaying;",
AppCategory::SimulationGame => "Game;Simulation;",
AppCategory::SportsGame => "Game;SportsGame;",
AppCategory::StrategyGame => "Game;StrategyGame;",
AppCategory::TriviaGame => "Game;",
AppCategory::WordGame => "Game;",
AppCategory::GraphicsAndDesign => "Graphics;",
AppCategory::HealthcareAndFitness => "Science;",
AppCategory::Lifestyle => "Education;",
AppCategory::Medical => "Science;MedicalSoftware;",
AppCategory::Music => "AudioVideo;Audio;Music;",
AppCategory::News => "Network;News;",
AppCategory::Photography => "Graphics;Photography;",
AppCategory::Productivity => "Office;",
AppCategory::Reference => "Education;",
AppCategory::SocialNetworking => "Network;",
AppCategory::Sports => "Education;Sports;",
AppCategory::Travel => "Education;",
AppCategory::Utility => "Utility;",
AppCategory::Video => "AudioVideo;Video;",
AppCategory::Weather => "Science;",
}
}
/// Map to macOS LSApplicationCategoryType.
pub(crate) fn macos_application_category_type(self) -> &'static str {
match &self {
AppCategory::Business => "public.app-category.business",
AppCategory::DeveloperTool => "public.app-category.developer-tools",
AppCategory::Education => "public.app-category.education",
AppCategory::Entertainment => "public.app-category.entertainment",
AppCategory::Finance => "public.app-category.finance",
AppCategory::Game => "public.app-category.games",
AppCategory::ActionGame => "public.app-category.action-games",
AppCategory::AdventureGame => "public.app-category.adventure-games",
AppCategory::ArcadeGame => "public.app-category.arcade-games",
AppCategory::BoardGame => "public.app-category.board-games",
AppCategory::CardGame => "public.app-category.card-games",
AppCategory::CasinoGame => "public.app-category.casino-games",
AppCategory::DiceGame => "public.app-category.dice-games",
AppCategory::EducationalGame => "public.app-category.educational-games",
AppCategory::FamilyGame => "public.app-category.family-games",
AppCategory::KidsGame => "public.app-category.kids-games",
AppCategory::MusicGame => "public.app-category.music-games",
AppCategory::PuzzleGame => "public.app-category.puzzle-games",
AppCategory::RacingGame => "public.app-category.racing-games",
AppCategory::RolePlayingGame => "public.app-category.role-playing-games",
AppCategory::SimulationGame => "public.app-category.simulation-games",
AppCategory::SportsGame => "public.app-category.sports-games",
AppCategory::StrategyGame => "public.app-category.strategy-games",
AppCategory::TriviaGame => "public.app-category.trivia-games",
AppCategory::WordGame => "public.app-category.word-games",
AppCategory::GraphicsAndDesign => "public.app-category.graphics-design",
AppCategory::HealthcareAndFitness => "public.app-category.healthcare-fitness",
AppCategory::Lifestyle => "public.app-category.lifestyle",
AppCategory::Medical => "public.app-category.medical",
AppCategory::Music => "public.app-category.music",
AppCategory::News => "public.app-category.news",
AppCategory::Photography => "public.app-category.photography",
AppCategory::Productivity => "public.app-category.productivity",
AppCategory::Reference => "public.app-category.reference",
AppCategory::SocialNetworking => "public.app-category.social-networking",
AppCategory::Sports => "public.app-category.sports",
AppCategory::Travel => "public.app-category.travel",
AppCategory::Utility => "public.app-category.utilities",
AppCategory::Video => "public.app-category.video",
AppCategory::Weather => "public.app-category.weather",
}
}
}
const CATEGORY_STRINGS: &[(&str, AppCategory)] = &[
("actiongame", AppCategory::ActionGame),
("actiongames", AppCategory::ActionGame),
("adventuregame", AppCategory::AdventureGame),
("adventuregames", AppCategory::AdventureGame),
("arcadegame", AppCategory::ArcadeGame),
("arcadegames", AppCategory::ArcadeGame),
("boardgame", AppCategory::BoardGame),
("boardgames", AppCategory::BoardGame),
("business", AppCategory::Business),
("cardgame", AppCategory::CardGame),
("cardgames", AppCategory::CardGame),
("casinogame", AppCategory::CasinoGame),
("casinogames", AppCategory::CasinoGame),
("developer", AppCategory::DeveloperTool),
("developertool", AppCategory::DeveloperTool),
("developertools", AppCategory::DeveloperTool),
("development", AppCategory::DeveloperTool),
("dicegame", AppCategory::DiceGame),
("dicegames", AppCategory::DiceGame),
("education", AppCategory::Education),
("educationalgame", AppCategory::EducationalGame),
("educationalgames", AppCategory::EducationalGame),
("entertainment", AppCategory::Entertainment),
("familygame", AppCategory::FamilyGame),
("familygames", AppCategory::FamilyGame),
("finance", AppCategory::Finance),
("fitness", AppCategory::HealthcareAndFitness),
("game", AppCategory::Game),
("games", AppCategory::Game),
("graphicdesign", AppCategory::GraphicsAndDesign),
("graphicsanddesign", AppCategory::GraphicsAndDesign),
("graphicsdesign", AppCategory::GraphicsAndDesign),
("healthcareandfitness", AppCategory::HealthcareAndFitness),
("healthcarefitness", AppCategory::HealthcareAndFitness),
("kidsgame", AppCategory::KidsGame),
("kidsgames", AppCategory::KidsGame),
("lifestyle", AppCategory::Lifestyle),
("logicgame", AppCategory::PuzzleGame),
("medical", AppCategory::Medical),
("music", AppCategory::Music),
("musicgame", AppCategory::MusicGame),
("musicgames", AppCategory::MusicGame),
("news", AppCategory::News),
("photography", AppCategory::Photography),
("productivity", AppCategory::Productivity),
("puzzlegame", AppCategory::PuzzleGame),
("puzzlegames", AppCategory::PuzzleGame),
("racinggame", AppCategory::RacingGame),
("racinggames", AppCategory::RacingGame),
("reference", AppCategory::Reference),
("roleplaying", AppCategory::RolePlayingGame),
("roleplayinggame", AppCategory::RolePlayingGame),
("roleplayinggames", AppCategory::RolePlayingGame),
("rpg", AppCategory::RolePlayingGame),
("simulationgame", AppCategory::SimulationGame),
("simulationgames", AppCategory::SimulationGame),
("socialnetwork", AppCategory::SocialNetworking),
("socialnetworking", AppCategory::SocialNetworking),
("sports", AppCategory::Sports),
("sportsgame", AppCategory::SportsGame),
("sportsgames", AppCategory::SportsGame),
("strategygame", AppCategory::StrategyGame),
("strategygames", AppCategory::StrategyGame),
("travel", AppCategory::Travel),
("triviagame", AppCategory::TriviaGame),
("triviagames", AppCategory::TriviaGame),
("utilities", AppCategory::Utility),
("utility", AppCategory::Utility),
("video", AppCategory::Video),
("weather", AppCategory::Weather),
("wordgame", AppCategory::WordGame),
("wordgames", AppCategory::WordGame),
];
/// Recursively copy a directory tree.
///
/// Preserves symlinks on unix targets and falls back to copying link targets on non-unix.
pub(crate) fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<()> {
std::fs::create_dir_all(dest)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let source_path = entry.path();
let dest_path = dest.join(entry.file_name());
let file_type = entry.file_type()?;
if file_type.is_dir() {
copy_dir_recursive(&source_path, &dest_path)?;
} else if file_type.is_symlink() {
#[cfg(unix)]
{
let target = std::fs::read_link(&source_path)?;
std::os::unix::fs::symlink(&target, &dest_path)?;
}
#[cfg(not(unix))]
{
std::fs::copy(&source_path, &dest_path)?;
}
} else {
std::fs::copy(&source_path, &dest_path)?;
}
}
Ok(())
}
/// Recursively zip a directory tree while preserving relative paths and Unix modes.
pub(crate) fn zip_dir_recursive(src: &Path, dest: &Path) -> Result<()> {
use std::fs::File;
use std::io::{Read, Write};
let file =
File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let mut zip = zip::ZipWriter::new(file);
for entry in walkdir::WalkDir::new(src) {
let entry = entry?;
let path = entry.path();
let relative = path
.strip_prefix(src)
.with_context(|| format!("Failed to strip prefix for {}", path.display()))?;
if relative.as_os_str().is_empty() {
continue;
}
let archive_path = relative.to_string_lossy().replace('\\', "/");
let metadata = entry.metadata()?;
#[cfg(unix)]
let mode = {
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode()
};
#[cfg(not(unix))]
let mode = if metadata.is_dir() { 0o755 } else { 0o644 };
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(mode);
if metadata.is_dir() {
zip.add_directory(format!("{archive_path}/"), options)?;
continue;
}
zip.start_file(&archive_path, options)?;
let mut src_file = File::open(path)?;
let mut buffer = Vec::new();
src_file.read_to_end(&mut buffer)?;
zip.write_all(&buffer)?;
}
zip.finish()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::zip_dir_recursive;
#[test]
fn zip_dir_preserves_layout_and_permissions() {
let temp = tempfile::tempdir().unwrap();
let src = temp.path().join("src");
let payload = src.join("Payload/Test.app");
std::fs::create_dir_all(&payload).unwrap();
let exec = payload.join("runner");
std::fs::write(&exec, "hello").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&exec, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let zip_path = temp.path().join("bundle.zip");
zip_dir_recursive(&src, &zip_path).unwrap();
let file = std::fs::File::open(&zip_path).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
let entry = archive.by_name("Payload/Test.app/runner").unwrap();
let expected_mode = if cfg!(unix) { 0o755 } else { 0o644 };
assert_eq!(
entry.unix_mode().map(|mode| mode & 0o777),
Some(expected_mode)
);
}
}
+305
View File
@@ -0,0 +1,305 @@
//! Tool downloading and caching for external bundling tools.
//!
//! All downloads happen upfront via `resolve_tools()` before any bundling starts.
//! This keeps tool downloads out of the bundle format modules.
use super::Arch;
use crate::{PackageType, WebviewInstallMode, WindowsSettings};
use anyhow::{bail, Context, Result};
use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
/// NSIS download URL and expected hash
const NSIS_URL: &str =
"https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.11/nsis-3.11.zip";
const NSIS_SHA1: &str = "EF7FF767E5CBD9EDD22ADD3A32C9B8F4500BB10D";
/// WiX download URL and expected hash
const WIX_URL: &str =
"https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip";
const WIX_SHA256: &str = "6ac824e1642d6f7277d0ed7ea09411a508f6116ba6fae0aa5f2c7daa2ff43d31";
/// linuxdeploy download base URL
const LINUXDEPLOY_URL_BASE: &str =
"https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy";
/// WebView2 download URLs.
const WEBVIEW2_BOOTSTRAPPER_URL: &str = "https://go.microsoft.com/fwlink/p/?LinkId=2124703";
const WEBVIEW2_X64_INSTALLER_URL: &str = "https://go.microsoft.com/fwlink/?linkid=2099617";
const WEBVIEW2_X86_INSTALLER_URL: &str = "https://go.microsoft.com/fwlink/?linkid=2099617";
const WEBVIEW2_ARM64_INSTALLER_URL: &str = "https://go.microsoft.com/fwlink/?linkid=2099617";
/// Pre-resolved tool paths. All downloads happen before bundling starts.
pub(crate) struct ResolvedTools {
/// Path to the NSIS directory (contains makensis). Set if NSIS bundling is requested.
pub nsis_dir: Option<PathBuf>,
/// Path to the WiX directory (contains candle.exe, light.exe). Set if MSI bundling is requested.
pub wix_dir: Option<PathBuf>,
/// Path to the linuxdeploy binary. Set if AppImage bundling is requested.
pub linuxdeploy: Option<PathBuf>,
/// Path to a downloaded WebView2 bootstrapper or offline installer, if needed by NSIS.
pub webview2_installer: Option<PathBuf>,
}
/// Resolve and download all tools needed for the given package types.
///
/// This must be called before `bundle_project()` so that no HTTP calls happen
/// during the actual bundling phase.
pub(crate) async fn resolve_tools(
tools_dir: &Path,
package_types: &[PackageType],
windows_settings: &WindowsSettings,
arch: Arch,
) -> Result<ResolvedTools> {
let mut resolved = ResolvedTools {
nsis_dir: None,
wix_dir: None,
linuxdeploy: None,
webview2_installer: None,
};
for pt in package_types {
match pt {
PackageType::Nsis => {
resolved.nsis_dir = Some(ensure_nsis(tools_dir).await?);
resolved.webview2_installer =
resolve_webview2(tools_dir, windows_settings, arch).await?;
}
PackageType::WindowsMsi => {
resolved.wix_dir = Some(ensure_wix(tools_dir).await?);
}
PackageType::AppImage => {
let linuxdeploy_arch = arch.linuxdeploy_arch();
resolved.linuxdeploy = Some(ensure_linuxdeploy(tools_dir, linuxdeploy_arch).await?);
}
_ => {}
}
}
Ok(resolved)
}
/// Determine if a WebView2 installer needs to be downloaded based on NSIS settings,
/// and download it if so.
async fn resolve_webview2(
tools_dir: &Path,
settings: &WindowsSettings,
arch: Arch,
) -> Result<Option<PathBuf>> {
let mode = &settings.webview_install_mode;
match mode {
WebviewInstallMode::Skip | WebviewInstallMode::FixedRuntime { .. } => Ok(None),
WebviewInstallMode::DownloadBootstrapper { .. }
| WebviewInstallMode::EmbedBootstrapper { .. } => {
Ok(Some(download_webview2_bootstrapper(tools_dir).await?))
}
WebviewInstallMode::OfflineInstaller { .. } => {
let arch_str = arch.windows_arch();
Ok(Some(
download_webview2_offline_installer(tools_dir, arch_str).await?,
))
}
}
}
async fn ensure_nsis(tools_dir: &Path) -> Result<PathBuf> {
let nsis_dir = tools_dir.join("nsis-3.11");
let makensis = if cfg!(target_os = "windows") {
nsis_dir.join("makensis.exe")
} else {
nsis_dir.join("makensis")
};
if makensis.exists() {
return Ok(nsis_dir);
}
if cfg!(feature = "no-downloads") {
bail!("NSIS not found and automatic downloads are disabled. Install NSIS manually.");
}
tracing::info!("Downloading NSIS...");
let data = download_and_verify(NSIS_URL, NSIS_SHA1, HashAlgo::Sha1).await?;
extract_zip(&data, tools_dir)?;
if !makensis.exists() {
bail!(
"NSIS extraction succeeded but makensis not found at {}",
makensis.display()
);
}
#[cfg(unix)]
let _ = std::fs::set_permissions(&makensis, std::fs::Permissions::from_mode(0o755));
Ok(nsis_dir)
}
async fn ensure_wix(tools_dir: &Path) -> Result<PathBuf> {
let wix_dir = tools_dir.join("wix314");
let candle = wix_dir.join("candle.exe");
if candle.exists() {
return Ok(wix_dir);
}
if cfg!(feature = "no-downloads") {
bail!("WiX not found and automatic downloads are disabled. Install WiX manually.");
}
tracing::info!("Downloading WiX toolset...");
let data = download_and_verify(WIX_URL, WIX_SHA256, HashAlgo::Sha256).await?;
std::fs::create_dir_all(&wix_dir)?;
extract_zip(&data, &wix_dir)?;
if !candle.exists() {
bail!(
"WiX extraction succeeded but candle.exe not found at {}",
candle.display()
);
}
Ok(wix_dir)
}
async fn ensure_linuxdeploy(tools_dir: &Path, arch: &str) -> Result<PathBuf> {
let linuxdeploy_name = format!("linuxdeploy-{arch}.AppImage");
let linuxdeploy_path = tools_dir.join(&linuxdeploy_name);
if linuxdeploy_path.exists() {
return Ok(linuxdeploy_path);
}
if cfg!(feature = "no-downloads") {
bail!(
"linuxdeploy not found and automatic downloads are disabled. Install linuxdeploy manually."
);
}
let url = format!("{LINUXDEPLOY_URL_BASE}/{linuxdeploy_name}");
tracing::info!("Downloading linuxdeploy from {url}...");
let data = download_bytes(&url).await?;
std::fs::create_dir_all(tools_dir)?;
std::fs::write(&linuxdeploy_path, &data)?;
#[cfg(unix)]
std::fs::set_permissions(&linuxdeploy_path, std::fs::Permissions::from_mode(0o755))?;
Ok(linuxdeploy_path)
}
async fn download_webview2_bootstrapper(tools_dir: &Path) -> Result<PathBuf> {
let path = tools_dir.join("MicrosoftEdgeWebview2Setup.exe");
if path.exists() {
return Ok(path);
}
tracing::info!("Downloading WebView2 bootstrapper...");
let data = download_bytes(WEBVIEW2_BOOTSTRAPPER_URL).await?;
std::fs::create_dir_all(tools_dir)?;
std::fs::write(&path, &data).context("Failed to write WebView2 bootstrapper")?;
Ok(path)
}
async fn download_webview2_offline_installer(tools_dir: &Path, arch: &str) -> Result<PathBuf> {
let name = format!("MicrosoftEdgeWebView2RuntimeInstaller_{arch}.exe");
let path = tools_dir.join(&name);
if path.exists() {
return Ok(path);
}
let url = match arch {
"x64" => WEBVIEW2_X64_INSTALLER_URL,
"x86" => WEBVIEW2_X86_INSTALLER_URL,
"arm64" => WEBVIEW2_ARM64_INSTALLER_URL,
_ => bail!("Unsupported architecture for WebView2 offline installer: {arch}"),
};
tracing::info!("Downloading WebView2 offline installer for {arch}...");
let data = download_bytes(url).await?;
std::fs::create_dir_all(tools_dir)?;
std::fs::write(&path, &data).context("Failed to write WebView2 offline installer")?;
Ok(path)
}
enum HashAlgo {
Sha1,
Sha256,
}
async fn download_and_verify(url: &str, expected_hash: &str, algo: HashAlgo) -> Result<Vec<u8>> {
let data = download_bytes(url).await?;
let computed = match algo {
HashAlgo::Sha1 => {
use sha1::Digest;
let mut hasher = sha1::Sha1::new();
hasher.update(&data);
format!("{:X}", hasher.finalize())
}
HashAlgo::Sha256 => {
use sha2::Digest;
let mut hasher = sha2::Sha256::new();
hasher.update(&data);
format!("{:x}", hasher.finalize())
}
};
if computed.to_uppercase() != expected_hash.to_uppercase() {
bail!("Hash mismatch for {url}: expected {expected_hash}, got {computed}");
}
Ok(data)
}
/// Download bytes from a URL using the async reqwest client.
pub(crate) async fn download_bytes(url: &str) -> Result<Vec<u8>> {
let response = reqwest::get(url)
.await
.with_context(|| format!("Failed to download {url}"))?;
if !response.status().is_success() {
bail!("Download failed with status {}: {url}", response.status());
}
response
.bytes()
.await
.map(|b| b.to_vec())
.with_context(|| format!("Failed to read response body from {url}"))
}
/// Extract a zip archive to a directory.
fn extract_zip(data: &[u8], dest: &Path) -> Result<()> {
use std::io::Cursor;
let cursor = Cursor::new(data);
let mut archive = zip::ZipArchive::new(cursor).context("Failed to read zip archive")?;
std::fs::create_dir_all(dest)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = dest.join(file.mangled_name());
if file.name().ends_with('/') {
std::fs::create_dir_all(&outpath)?;
} else {
if let Some(parent) = outpath.parent() {
std::fs::create_dir_all(parent)?;
}
let mut outfile = std::fs::File::create(&outpath)?;
std::io::copy(&mut file, &mut outfile)?;
#[cfg(unix)]
if let Some(mode) = file.unix_mode() {
std::fs::set_permissions(&outpath, std::fs::Permissions::from_mode(mode))?;
}
}
}
Ok(())
}
+152
View File
@@ -0,0 +1,152 @@
//! Updater bundle creation.
//!
//! Creates zip/tar.gz archives of bundle artifacts for auto-update distribution.
use super::{Bundle, BundleContext};
use crate::PackageType;
use anyhow::{Context, Result};
use std::{
fs::File,
io,
path::{Path, PathBuf},
};
impl BundleContext<'_> {
/// Repackage previously-built bundle artifacts into updater-friendly archives.
///
/// This step never builds the application itself. Instead, it consumes the
/// [`Bundle`] records produced earlier in the main bundling pass and wraps those
/// artifacts into archive formats that are convenient for update distribution.
///
/// Archive mapping:
/// - macOS `.app` bundles become `.app.tar.gz` archives so the bundle directory
/// layout is preserved exactly.
/// - Windows installers (`.msi` and NSIS `.exe`) become single-file `.zip`
/// archives.
/// - Linux artifacts (`.AppImage` and `.deb`) become single-file `.tar.gz`
/// archives.
///
/// All outputs are written to `project_out_directory()/bundle/updater`. This
/// method assumes the input bundles are already finalized and signed as needed; it
/// performs no platform-specific mutation beyond wrapping them in the selected
/// archive container.
pub(crate) async fn bundle_updater(&self, bundles: &[Bundle]) -> Result<Vec<PathBuf>> {
let mut updater_paths = Vec::new();
let output_dir = self.project_out_directory().join("bundle").join("updater");
std::fs::create_dir_all(&output_dir)?;
for bundle in bundles {
match bundle.package_type {
PackageType::MacOsBundle => {
// Create .tar.gz of the .app bundle
for app_path in &bundle.bundle_paths {
let tar_path = output_dir.join(format!(
"{}_{}.app.tar.gz",
self.product_name(),
self.version_string()
));
create_tar_gz(app_path, &tar_path)?;
tracing::info!("Created updater archive: {}", tar_path.display());
updater_paths.push(tar_path);
}
}
PackageType::Nsis | PackageType::WindowsMsi => {
// Create .zip of the installer
for installer_path in &bundle.bundle_paths {
let zip_path = output_dir.join(format!(
"{}.zip",
installer_path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
));
create_zip(installer_path, &zip_path)?;
tracing::info!("Created updater archive: {}", zip_path.display());
updater_paths.push(zip_path);
}
}
PackageType::AppImage | PackageType::Deb => {
// Create .tar.gz of the artifact
for artifact_path in &bundle.bundle_paths {
let tar_path = output_dir.join(format!(
"{}.tar.gz",
artifact_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
));
create_tar_gz_single_file(artifact_path, &tar_path)?;
tracing::info!("Created updater archive: {}", tar_path.display());
updater_paths.push(tar_path);
}
}
_ => {}
}
}
Ok(updater_paths)
}
}
/// Create a .tar.gz of a directory (e.g., a .app bundle).
fn create_tar_gz(src_dir: &Path, dest: &Path) -> Result<()> {
let file =
File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let enc = flate2::write::GzEncoder::new(file, flate2::Compression::default());
let mut tar = tar::Builder::new(enc);
let dir_name = src_dir
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
tar.append_dir_all(&dir_name, src_dir)
.with_context(|| format!("Failed to add {} to tar", src_dir.display()))?;
tar.into_inner()?.finish()?;
Ok(())
}
/// Create a .tar.gz containing a single file.
fn create_tar_gz_single_file(src_file: &Path, dest: &Path) -> Result<()> {
let file =
File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let enc = flate2::write::GzEncoder::new(file, flate2::Compression::default());
let mut tar = tar::Builder::new(enc);
let file_name = src_file
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
tar.append_path_with_name(src_file, &file_name)
.with_context(|| format!("Failed to add {} to tar", src_file.display()))?;
tar.into_inner()?.finish()?;
Ok(())
}
/// Create a .zip containing a single file.
fn create_zip(src_file: &Path, dest: &Path) -> Result<()> {
let file =
File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let mut zip = zip::ZipWriter::new(file);
let file_name = src_file
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
zip.start_file(&file_name, options)?;
let mut src = File::open(src_file)?;
io::copy(&mut src, &mut zip)?;
zip.finish()?;
Ok(())
}
File diff suppressed because it is too large. Load diff
+15 -15
View File
@@ -67,43 +67,43 @@ fn extend_manifest_config_toml(manifest: &mut Manifest, path: &Path) {
/// Merge the new profiles into the target profiles. Keep the existing values if they exist. /// Merge the new profiles into the target profiles. Keep the existing values if they exist.
fn merge_profiles(target: &mut Profiles, new: Profiles) { fn merge_profiles(target: &mut Profiles, new: Profiles) {
if let Some(new_release) = new.release { if let Some(new_release) = new.release {
if target.release.is_none() { if let Some(profile) = target.release.as_mut() {
target.release = Some(new_release); merge_profile(profile, new_release);
} else { } else {
merge_profile(target.release.as_mut().unwrap(), new_release); target.release = Some(new_release);
} }
} }
if let Some(new_dev) = new.dev { if let Some(new_dev) = new.dev {
if target.dev.is_none() { if let Some(profile) = target.dev.as_mut() {
target.dev = Some(new_dev); merge_profile(profile, new_dev);
} else { } else {
merge_profile(target.dev.as_mut().unwrap(), new_dev); target.dev = Some(new_dev);
} }
} }
if let Some(new_test) = new.test { if let Some(new_test) = new.test {
if target.test.is_none() { if let Some(profile) = target.test.as_mut() {
target.test = Some(new_test); merge_profile(profile, new_test);
} else { } else {
merge_profile(target.test.as_mut().unwrap(), new_test); target.test = Some(new_test);
} }
} }
if let Some(new_bench) = new.bench { if let Some(new_bench) = new.bench {
if target.bench.is_none() { if let Some(profile) = target.bench.as_mut() {
target.bench = Some(new_bench); merge_profile(profile, new_bench);
} else { } else {
merge_profile(target.bench.as_mut().unwrap(), new_bench); target.bench = Some(new_bench);
} }
} }
#[allow(deprecated)] #[allow(deprecated)]
if let Some(new_doc) = new.doc { if let Some(new_doc) = new.doc {
if target.doc.is_none() { if let Some(profile) = target.doc.as_mut() {
target.doc = Some(new_doc); merge_profile(profile, new_doc);
} else { } else {
merge_profile(target.doc.as_mut().unwrap(), new_doc); target.doc = Some(new_doc);
} }
} }
@@ -7,7 +7,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use crate::metadata::{ use super::metadata::{
AnyLoopInfo, AsyncInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, MatchInfo, AnyLoopInfo, AsyncInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, MatchInfo,
WhileInfo, WhileInfo,
}; };
@@ -229,7 +229,7 @@ impl std::fmt::Display for Issue {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::check_file; use crate::check::check_file;
use indoc::indoc; use indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -168,6 +168,7 @@ pub struct Span {
} }
impl Span { impl Span {
#[cfg(test)]
pub fn new_from_str(source_text: &str, start: LineColumn) -> Self { pub fn new_from_str(source_text: &str, start: LineColumn) -> Self {
let mut lines = source_text.lines(); let mut lines = source_text.lines();
let first_line = lines.next().unwrap_or_default(); let first_line = lines.next().unwrap_or_default();
@@ -1,8 +1,10 @@
use std::path::PathBuf; mod issues;
mod metadata;
use std::path::PathBuf;
use syn::{spanned::Spanned, visit::Visit, Pat}; use syn::{spanned::Spanned, visit::Visit, Pat};
use crate::{ use {
issues::{Issue, IssueReport}, issues::{Issue, IssueReport},
metadata::{ metadata::{
AnyLoopInfo, AsyncInfo, ClosureInfo, ComponentInfo, ConditionalInfo, FnInfo, ForInfo, AnyLoopInfo, AsyncInfo, ClosureInfo, ComponentInfo, ConditionalInfo, FnInfo, ForInfo,
@@ -289,7 +291,7 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::metadata::{ use crate::check::metadata::{
AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, LineColumn, LoopInfo, AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, LineColumn, LoopInfo,
MatchInfo, Span, WhileInfo, MatchInfo, Span, WhileInfo,
}; };
+2 -2
View File
@@ -1,8 +1,8 @@
use std::{fs::create_dir_all, path::PathBuf}; use std::{fs::create_dir_all, path::PathBuf};
use crate::opt::process_file_to;
use crate::{extract_assets_from_file, Result, StructuredOutput}; use crate::{extract_assets_from_file, Result, StructuredOutput};
use clap::Parser; use clap::Parser;
use dioxus_cli_opt::process_file_to;
use tracing::debug; use tracing::debug;
#[derive(Clone, Debug, Parser)] #[derive(Clone, Debug, Parser)]
@@ -28,7 +28,7 @@ impl BuildAssets {
destination_path.display(), destination_path.display(),
asset asset
); );
process_file_to(asset.options(), &source_path, &destination_path)?; process_file_to(asset.options(), &source_path, &destination_path, None)?;
} }
Ok(StructuredOutput::Success) Ok(StructuredOutput::Success)
+177 -162
View File
@@ -1,10 +1,10 @@
use crate::{AppBuilder, BuildArgs, BuildId, BuildMode, BuildRequest, BundleFormat}; use crate::{
use anyhow::{bail, Context}; AppBuilder, BuildArgs, BuildId, BuildMode, BuildRequest, BundleFormat, PackageType, Platform,
};
use anyhow::Context;
use path_absolutize::Absolutize; use path_absolutize::Absolutize;
use std::collections::HashMap; use std::path::PathBuf;
use tauri_bundler::{BundleBinary, BundleSettings, PackageSettings, SettingsBuilder}; use target_lexicon::Triple;
use walkdir::WalkDir;
use super::*; use super::*;
@@ -15,7 +15,7 @@ use super::*;
pub struct Bundle { pub struct Bundle {
/// The package types to bundle /// The package types to bundle
#[clap(long)] #[clap(long)]
pub package_types: Option<Vec<crate::PackageType>>, pub package_types: Option<Vec<PackageType>>,
/// The directory in which the final bundle will be placed. /// The directory in which the final bundle will be placed.
/// ///
@@ -37,15 +37,14 @@ impl Bundle {
pub(crate) async fn bundle(mut self) -> Result<StructuredOutput> { pub(crate) async fn bundle(mut self) -> Result<StructuredOutput> {
tracing::info!("Bundling project..."); tracing::info!("Bundling project...");
self.force_ios_bundle_device_target_if_needed()?;
let BuildTargets { client, server } = self.args.into_targets().await?; let BuildTargets { client, server } = self.args.into_targets().await?;
let mut server_artifacts = None;
let client_artifacts = let client_artifacts =
AppBuilder::started(&client, BuildMode::Base { run: false }, BuildId::PRIMARY)? AppBuilder::started(&client, BuildMode::Base { run: false }, BuildId::PRIMARY)?
.finish_build() .finish_build()
.await?; .await?;
let mut server_artifacts = None;
tracing::info!(path = ?client.root_dir(), "Client build completed successfully! 🚀");
if let Some(server) = server.as_ref() { if let Some(server) = server.as_ref() {
// If the server is present, we need to build it as well // If the server is present, we need to build it as well
@@ -58,11 +57,13 @@ impl Bundle {
tracing::info!(path = ?client.root_dir(), "Server build completed successfully! 🚀"); tracing::info!(path = ?client.root_dir(), "Server build completed successfully! 🚀");
} }
// If we're building for iOS, we need to bundle the iOS bundle // Fill platform-specific defaults for package types when omitted.
if client.bundle == BundleFormat::Ios && self.package_types.is_none() { if self.package_types.is_none() {
self.package_types = Some(vec![crate::PackageType::IosBundle]); self.package_types = Self::default_package_types(client.bundle);
} }
Self::validate_package_types_for_bundle(client.bundle, self.package_types.as_deref())?;
let mut bundles = vec![]; let mut bundles = vec![];
// Copy the server over if it exists // Copy the server over if it exists
@@ -70,31 +71,24 @@ impl Bundle {
bundles.push(server.main_exe()); bundles.push(server.main_exe());
} }
// Create a list of bundles that we might need to copy // Create a list of bundles that we might need to copy.
// Package-type based bundling is handled by the bundler module.
match client.bundle { match client.bundle {
// By default, mac/win/linux work with tauri bundle // Desktop and Android platforms use package-type dispatch in the bundler module.
BundleFormat::MacOS | BundleFormat::Linux | BundleFormat::Windows => { BundleFormat::MacOS
tracing::info!("Running desktop bundler..."); | BundleFormat::Ios
for bundle in Self::bundle_desktop(&client, &self.package_types)? { | BundleFormat::Linux
| BundleFormat::Windows
| BundleFormat::Android => {
tracing::info!("Running package bundler...");
for bundle in Self::bundle_with_package_types(&client, &self.package_types).await? {
bundles.extend(bundle.bundle_paths); bundles.extend(bundle.bundle_paths);
} }
} }
// Web/ios can just use their root_dir // Web can use its root_dir directly.
BundleFormat::Web => bundles.push(client.root_dir()), BundleFormat::Web => bundles.push(client.root_dir()),
BundleFormat::Ios => {
tracing::warn!("iOS bundles are not currently codesigned! You will need to codesign the app before distributing.");
bundles.push(client.root_dir())
}
BundleFormat::Server => bundles.push(client.root_dir()), BundleFormat::Server => bundles.push(client.root_dir()),
BundleFormat::Android => {
let aab = client
.android_gradle_bundle()
.await
.context("Failed to run gradle bundleRelease")?;
bundles.push(aab);
}
}; };
// Copy the bundles to the output directory if one was specified // Copy the bundles to the output directory if one was specified
@@ -148,147 +142,45 @@ impl Bundle {
}) })
} }
fn bundle_desktop( async fn bundle_with_package_types(
build: &BuildRequest, build: &BuildRequest,
package_types: &Option<Vec<crate::PackageType>>, package_types: &Option<Vec<PackageType>>,
) -> Result<Vec<tauri_bundler::Bundle>, Error> { ) -> Result<Vec<crate::bundler::Bundle>, Error> {
let krate = &build; use anyhow::bail;
let exe = build.main_exe();
_ = std::fs::remove_dir_all(krate.bundle_dir(build.bundle)); if matches!(
build.bundle,
BundleFormat::MacOS | BundleFormat::Linux | BundleFormat::Windows
) {
let krate = &build;
let exe = build.main_exe();
let package = krate.package(); _ = std::fs::remove_dir_all(krate.bundle_dir(build.bundle));
let mut name: PathBuf = krate.executable_name().into();
if cfg!(windows) {
name.set_extension("exe");
}
std::fs::create_dir_all(krate.bundle_dir(build.bundle))
.context("Failed to create bundle directory")?;
std::fs::copy(&exe, krate.bundle_dir(build.bundle).join(&name))
.with_context(|| "Failed to copy the output executable into the bundle directory")?;
let binaries = vec![ let mut name: PathBuf = krate.executable_name().into();
// We use the name of the exe but it has to be in the same directory if cfg!(windows) {
BundleBinary::new(krate.executable_name().to_string(), true) name.set_extension("exe");
.set_src_path(Some(exe.display().to_string())), }
]; std::fs::create_dir_all(krate.bundle_dir(build.bundle))
.context("Failed to create bundle directory")?;
std::fs::copy(&exe, krate.bundle_dir(build.bundle).join(&name)).with_context(|| {
"Failed to copy the output executable into the bundle directory"
})?;
let mut bundle_settings: BundleSettings = krate.config.bundle.clone().into(); // Check if required fields are provided instead of failing silently.
if build.config.bundle.identifier.is_none() {
// Check if required fields are provided instead of failing silently. bail!("\n\nBundle identifier was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\nidentifier = \"com.mycompany\"\n\n");
if bundle_settings.identifier.is_none() { }
bail!("\n\nBundle identifier was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\nidentifier = \"com.mycompany\"\n\n"); if build.config.bundle.publisher.is_none() {
} bail!("\n\nBundle publisher was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\npublisher = \"MyCompany\"\n\n");
if bundle_settings.publisher.is_none() {
bail!("\n\nBundle publisher was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\npublisher = \"MyCompany\"\n\n");
}
/// Resolve an icon path relative to the crate dir
fn canonicalize_icon_path(build: &BuildRequest, icon: &mut String) -> Result<(), Error> {
let icon_path = build
.crate_dir()
.join(&icon)
.canonicalize()
.with_context(|| format!("Failed to canonicalize path to icon {icon:?}"))?;
*icon = icon_path.to_string_lossy().to_string();
Ok(())
}
// Resolve bundle.icon relative to the crate dir
if let Some(icons) = bundle_settings.icon.as_mut() {
for icon in icons.iter_mut() {
canonicalize_icon_path(build, icon)?;
} }
} }
#[allow(deprecated)] let ctx = crate::bundler::BundleContext::new(build, package_types).await?;
if cfg!(windows) {
// Resolve bundle.windows.icon_path relative to the crate dir
let mut windows_icon_path = bundle_settings
.windows
.icon_path
.to_string_lossy()
.to_string();
canonicalize_icon_path(build, &mut windows_icon_path)?;
bundle_settings.windows.icon_path = PathBuf::from(&windows_icon_path);
let windows_icon_override = krate.config.bundle.windows.as_ref().map(|w| &w.icon_path); tracing::debug!("Bundling project for {:?}", ctx.package_types());
if windows_icon_override.is_none() {
let icon_path = bundle_settings
.icon
.as_ref()
.and_then(|icons| icons.first());
if let Some(icon_path) = icon_path { let bundles = ctx.bundle_project().await.inspect_err(|err| {
bundle_settings.icon = Some(vec![icon_path.into()]);
};
}
}
if bundle_settings.resources_map.is_none() {
bundle_settings.resources_map = Some(HashMap::new());
}
let asset_dir = build.asset_dir();
if asset_dir.exists() {
for entry in WalkDir::new(&asset_dir) {
let entry = entry.unwrap();
let path = entry.path();
if path.is_file() {
let old = path
.canonicalize()
.with_context(|| format!("Failed to canonicalize {entry:?}"))?;
let new =
PathBuf::from("assets").join(path.strip_prefix(&asset_dir).unwrap_or(path));
tracing::debug!("Bundled asset: {old:?} -> {new:?}");
bundle_settings
.resources_map
.as_mut()
.expect("to be set")
.insert(old.display().to_string(), new.display().to_string());
}
}
}
for resource_path in bundle_settings.resources.take().into_iter().flatten() {
bundle_settings
.resources_map
.as_mut()
.expect("to be set")
.insert(resource_path, "".to_string());
}
let mut settings = SettingsBuilder::new()
.project_out_directory(krate.bundle_dir(build.bundle))
.package_settings(PackageSettings {
product_name: krate.bundled_app_name(),
version: package.version.to_string(),
description: package.description.clone().unwrap_or_default(),
homepage: Some(package.homepage.clone().unwrap_or_default()),
authors: Some(package.authors.clone()),
default_run: Some(name.display().to_string()),
})
.log_level(log::Level::Debug)
.binaries(binaries)
.bundle_settings(bundle_settings);
if let Some(packages) = &package_types {
settings = settings.package_types(packages.iter().map(|p| (*p).into()).collect());
}
settings = settings.target(build.triple.to_string());
let settings = settings
.build()
.context("failed to bundle tauri bundle settings")?;
tracing::debug!("Bundling project with settings: {:#?}", settings);
if cfg!(target_os = "macos") {
std::env::set_var("CI", "true");
}
let bundles = tauri_bundler::bundle::bundle_project(&settings).inspect_err(|err| {
tracing::error!("Failed to bundle project: {:#?}", err); tracing::error!("Failed to bundle project: {:#?}", err);
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
tracing::error!("Make sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)"); tracing::error!("Make sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)");
@@ -297,4 +189,127 @@ impl Bundle {
Ok(bundles) Ok(bundles)
} }
fn default_package_types(bundle: BundleFormat) -> Option<Vec<PackageType>> {
match bundle {
BundleFormat::Ios => Some(vec![crate::PackageType::Ipa]),
BundleFormat::Android => Some(vec![crate::PackageType::Aab]),
_ => None,
}
}
fn force_ios_bundle_device_target_if_needed(&mut self) -> Result<()> {
let client_args = self
.args
.client
.as_mut()
.map(|args| &mut args.build_arguments)
.unwrap_or(&mut self.args.shared.build_arguments);
if client_args.platform == Platform::Ios && client_args.target.is_none() {
client_args.target = Some("aarch64-apple-ios".parse::<Triple>()?);
}
Ok(())
}
fn validate_package_types_for_bundle(
bundle: BundleFormat,
package_types: Option<&[PackageType]>,
) -> Result<(), Error> {
let Some(package_types) = package_types else {
return Ok(());
};
let is_package_supported = |package_type: &PackageType| -> bool {
match bundle {
BundleFormat::MacOS => matches!(
package_type,
PackageType::MacOsBundle | PackageType::Dmg | PackageType::Updater
),
BundleFormat::Linux => matches!(
package_type,
PackageType::Deb
| PackageType::Rpm
| PackageType::AppImage
| PackageType::Updater
),
BundleFormat::Windows => {
matches!(
package_type,
PackageType::WindowsMsi | PackageType::Nsis | PackageType::Updater
)
}
BundleFormat::Android => {
matches!(package_type, PackageType::Apk | PackageType::Aab)
}
BundleFormat::Ios => matches!(package_type, PackageType::IosApp | PackageType::Ipa),
BundleFormat::Web | BundleFormat::Server => false,
}
};
if let Some(invalid) = package_types.iter().find(|pt| !is_package_supported(pt)) {
anyhow::bail!(
"Package type '{invalid:?}' is not supported for bundle format '{bundle}'."
);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::Bundle;
use crate::{
cli::CommandWithPlatformOverrides, BuildArgs, BundleFormat, PackageType, Platform,
};
#[test]
fn ios_bundle_defaults_to_ipa() {
assert_eq!(
Bundle::default_package_types(BundleFormat::Ios),
Some(vec![PackageType::Ipa])
);
}
#[test]
fn validates_ios_package_types() {
assert!(Bundle::validate_package_types_for_bundle(
BundleFormat::Ios,
Some(&[PackageType::IosApp, PackageType::Ipa]),
)
.is_ok());
assert!(Bundle::validate_package_types_for_bundle(
BundleFormat::Ios,
Some(&[PackageType::Dmg]),
)
.is_err());
}
#[test]
fn bundle_ios_defaults_to_device_target() {
let mut bundle = Bundle {
package_types: None,
out_dir: None,
args: CommandWithPlatformOverrides {
shared: BuildArgs {
build_arguments: crate::TargetArgs {
platform: Platform::Ios,
..Default::default()
},
..Default::default()
},
..Default::default()
},
};
bundle.force_ios_bundle_device_target_if_needed().unwrap();
assert_eq!(
bundle.args.shared.build_arguments.target,
Some("aarch64-apple-ios".parse().unwrap())
);
}
} }
+1 -1
View File
@@ -84,7 +84,7 @@ async fn check_files_and_report(files_to_check: Vec<PathBuf>) -> Result<()> {
let res = tokio::spawn(async move { let res = tokio::spawn(async move {
tokio::fs::read_to_string(&_path) tokio::fs::read_to_string(&_path)
.await .await
.map(|contents| dioxus_check::check_file(_path, &contents)) .map(|contents| crate::check::check_file(_path, &contents))
}) })
.await; .await;
+60 -1
View File
@@ -63,7 +63,7 @@ where
{ {
fn augment_args(cmd: clap::Command) -> clap::Command { fn augment_args(cmd: clap::Command) -> clap::Command {
T::augment_args(cmd).defer(|cmd| { T::augment_args(cmd).defer(|cmd| {
PlatformOverrides::<Self>::augment_subcommands(cmd.disable_help_subcommand(true)) PlatformOverrides::<T>::augment_subcommands(cmd.disable_help_subcommand(true))
}) })
} }
@@ -182,3 +182,62 @@ where
unimplemented!() unimplemented!()
} }
} }
#[cfg(test)]
mod tests {
use clap::Parser;
#[test]
fn cli_parses_bundle_with_package_types_without_recursing() {
let cli = crate::Cli::try_parse_from([
"dx",
"bundle",
"--platform",
"desktop",
"--package-types",
"msi",
])
.expect("bundle command should parse");
assert!(matches!(cli.action, crate::Commands::Bundle(_)));
}
#[test]
fn cli_parses_platform_override_subcommands_without_recursing() {
let cli = crate::Cli::try_parse_from([
"dx",
"bundle",
"--platform",
"desktop",
"@client",
"--release",
"@server",
"--platform",
"server",
])
.expect("platform override subcommands should parse");
let crate::Commands::Bundle(bundle) = cli.action else {
panic!("expected bundle command");
};
let client = bundle
.args
.client
.as_ref()
.expect("@client override should be captured");
let server = bundle
.args
.server
.as_ref()
.expect("@server override should be captured");
assert!(!bundle.args.shared.build_arguments.release);
assert!(client.build_arguments.release);
assert_eq!(
server.build_arguments.platform,
crate::Platform::Server,
"@server should override the platform"
);
}
}
+3 -5
View File
@@ -178,8 +178,6 @@ impl Print {
/// ///
/// This function comes from shell-escape /// This function comes from shell-escape
fn escape_windows(s: Cow<str>) -> Cow<str> { fn escape_windows(s: Cow<str>) -> Cow<str> {
use std::iter::repeat;
let mut needs_escape = s.is_empty(); let mut needs_escape = s.is_empty();
for ch in s.chars() { for ch in s.chars() {
match ch { match ch {
@@ -202,15 +200,15 @@ impl Print {
match chars.next() { match chars.next() {
Some('"') => { Some('"') => {
es.extend(repeat('\\').take(nslashes * 2 + 1)); es.extend(std::iter::repeat_n('\\', nslashes * 2 + 1));
es.push('"'); es.push('"');
} }
Some(c) => { Some(c) => {
es.extend(repeat('\\').take(nslashes)); es.extend(std::iter::repeat_n('\\', nslashes));
es.push(c); es.push(c);
} }
None => { None => {
es.extend(repeat('\\').take(nslashes * 2)); es.extend(std::iter::repeat_n('\\', nslashes * 2));
break; break;
} }
} }
+6 -3
View File
@@ -82,10 +82,13 @@ impl RunArgs {
current, current,
total, total,
krate, krate,
fresh,
} => { } => {
tracing::debug!( if !fresh {
"[{bundle_format}] ({current}/{total}) Compiling {krate} ", tracing::debug!(
) "[{bundle_format}] ({current}/{total}) Compiling {krate} ",
)
}
} }
BuildStage::RunningBindgen => { BuildStage::RunningBindgen => {
tracing::info!("[{bundle_format}] Running WASM bindgen") tracing::info!("[{bundle_format}] Running WASM bindgen")
+33 -8
View File
@@ -121,7 +121,7 @@ pub(crate) struct WixSettings {
pub(crate) fips_compliant: bool, pub(crate) fips_compliant: bool,
/// MSI installer version in the format `major.minor.patch.build` (build is optional). /// MSI installer version in the format `major.minor.patch.build` (build is optional).
/// ///
/// Because a valid version is required for MSI installer, it will be derived from [`tauri_bundler::PackageSettings::version`] if this field is not set. /// Because a valid version is required for MSI installer, it will be derived from the package version if this field is not set.
/// ///
/// The first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255. /// The first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255.
/// The third and fourth fields have a maximum value of 65,535. /// The third and fourth fields have a maximum value of 65,535.
@@ -132,10 +132,9 @@ pub(crate) struct WixSettings {
/// A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**, /// A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**,
/// otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app. /// otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app.
/// ///
/// By default, tauri generates this code by generating a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace. /// By default, this code is generated as a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace.
/// You can use Tauri's CLI to generate and print this code for you by running `tauri inspect wix-upgrade-code`.
/// ///
/// It is recommended that you set this value in your tauri config file to avoid accidental changes in your upgrade code /// It is recommended that you set this value in your Dioxus.toml to avoid accidental changes in your upgrade code
/// whenever you want to change your product name. /// whenever you want to change your product name.
#[serde(default)] #[serde(default)]
#[schemars(with = "Option<String>")] #[schemars(with = "Option<String>")]
@@ -146,28 +145,39 @@ pub(crate) struct WixSettings {
pub(crate) struct MacOsSettings { pub(crate) struct MacOsSettings {
#[serde(default)] #[serde(default)]
pub(crate) bundle_version: Option<String>, pub(crate) bundle_version: Option<String>,
#[serde(default)] #[serde(default)]
pub(crate) frameworks: Option<Vec<String>>, pub(crate) frameworks: Option<Vec<String>>,
#[serde(default)] #[serde(default)]
pub(crate) minimum_system_version: Option<String>, pub(crate) minimum_system_version: Option<String>,
#[serde(default)] #[serde(default)]
pub(crate) license: Option<String>, pub(crate) license: Option<String>,
#[serde(default)] #[serde(default)]
pub(crate) exception_domain: Option<String>, pub(crate) exception_domain: Option<String>,
#[serde(default)] #[serde(default)]
pub(crate) signing_identity: Option<String>, pub(crate) signing_identity: Option<String>,
#[serde(default)] #[serde(default)]
pub(crate) provider_short_name: Option<String>, pub(crate) provider_short_name: Option<String>,
#[serde(default)] #[serde(default)]
pub(crate) entitlements: Option<String>, pub(crate) entitlements: Option<String>,
#[serde(default)] #[serde(default)]
pub(crate) info_plist_path: Option<PathBuf>, pub(crate) info_plist_path: Option<PathBuf>,
#[serde(default)] #[serde(default)]
pub(crate) bundle_name: Option<String>, pub(crate) bundle_name: Option<String>,
/// List of custom files to add to the application bundle. /// List of custom files to add to the application bundle.
/// Maps the path in the Contents directory in the app to the path of the file to include (relative to the current working directory). /// Maps the path in the Contents directory in the app to the path of the file to include (relative to the current working directory).
#[serde(default)] #[serde(default)]
pub files: HashMap<PathBuf, PathBuf>, pub files: HashMap<PathBuf, PathBuf>,
/// Preserve the hardened runtime version flag, see <https://developer.apple.com/documentation/security/hardened_runtime> /// Preserve the hardened runtime version flag, see <https://developer.apple.com/documentation/security/hardened_runtime>
/// ///
/// Settings this to `false` is useful when using an ad-hoc signature, making it less strict. /// Settings this to `false` is useful when using an ad-hoc signature, making it less strict.
@@ -290,15 +300,19 @@ pub struct CustomSignCommandSettings {
pub args: Vec<String>, pub args: Vec<String>,
} }
#[derive(Clone, Copy, Debug, clap::ValueEnum, Serialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum, Serialize)]
pub(crate) enum PackageType { pub(crate) enum PackageType {
/// The macOS application bundle (.app). /// The macOS application bundle (.app).
#[clap(name = "macos")] #[clap(name = "macos")]
MacOsBundle, MacOsBundle,
/// The iOS app bundle. /// The raw iOS application bundle (.app).
#[clap(name = "ios")] #[clap(name = "ios")]
IosBundle, IosApp,
/// The distributable iOS application archive (.ipa).
#[clap(name = "ipa")]
Ipa,
/// The Windows bundle (.msi). /// The Windows bundle (.msi).
#[clap(name = "msi")] #[clap(name = "msi")]
@@ -327,6 +341,14 @@ pub(crate) enum PackageType {
/// The Updater bundle (a patch of an existing app) /// The Updater bundle (a patch of an existing app)
#[clap(name = "updater")] #[clap(name = "updater")]
Updater, Updater,
/// Android application package (.apk).
#[clap(name = "apk")]
Apk,
/// Android App Bundle (.aab).
#[clap(name = "aab")]
Aab,
} }
impl FromStr for PackageType { impl FromStr for PackageType {
@@ -335,7 +357,8 @@ impl FromStr for PackageType {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"macos" => Ok(PackageType::MacOsBundle), "macos" => Ok(PackageType::MacOsBundle),
"ios" => Ok(PackageType::IosBundle), "ios" => Ok(PackageType::IosApp),
"ipa" => Ok(PackageType::Ipa),
"msi" => Ok(PackageType::WindowsMsi), "msi" => Ok(PackageType::WindowsMsi),
"nsis" => Ok(PackageType::Nsis), "nsis" => Ok(PackageType::Nsis),
"deb" => Ok(PackageType::Deb), "deb" => Ok(PackageType::Deb),
@@ -343,6 +366,8 @@ impl FromStr for PackageType {
"appimage" => Ok(PackageType::AppImage), "appimage" => Ok(PackageType::AppImage),
"dmg" => Ok(PackageType::Dmg), "dmg" => Ok(PackageType::Dmg),
"updater" => Ok(PackageType::Updater), "updater" => Ok(PackageType::Updater),
"apk" => Ok(PackageType::Apk),
"aab" => Ok(PackageType::Aab),
_ => Err(format!("{s} is not a valid package type")), _ => Err(format!("{s} is not a valid package type")),
} }
} }
+8 -2
View File
@@ -1,2 +1,8 @@
// The file has been placed there by the build script. pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
include!(concat!(env!("OUT_DIR"), "/built.rs")); pub const PKG_VERSION_MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
pub const PKG_VERSION_MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");
pub const PKG_VERSION_PATCH: &str = env!("CARGO_PKG_VERSION_PATCH");
pub const PKG_VERSION_PRE: &str = env!("CARGO_PKG_VERSION_PRE");
pub const GIT_COMMIT_HASH: Option<&str> = option_env!("DIOXUS_CLI_GIT_SHA");
pub const GIT_COMMIT_HASH_SHORT: Option<&str> = option_env!("DIOXUS_CLI_GIT_SHA_SHORT");
+207
View File
@@ -0,0 +1,207 @@
//! esbuild binary download, caching, and invocation.
//!
//! esbuild is used for JavaScript bundling and minification, replacing the
//! previous SWC Rust library dependencies. The standalone Go binary is
//! downloaded from the npm registry and cached locally.
use crate::{CliSettings, Workspace};
use anyhow::{anyhow, Context};
use std::path::PathBuf;
/// Pinned esbuild version.
const ESBUILD_VERSION: &str = "0.27.3";
pub(crate) struct Esbuild;
impl Esbuild {
/// Ensure the esbuild binary is available, downloading if needed.
/// Returns the path to the binary.
pub(crate) async fn get_or_install() -> anyhow::Result<PathBuf> {
if CliSettings::prefer_no_downloads() {
which::which("esbuild")
.map_err(|_| anyhow!("esbuild not found on PATH and downloads are disabled"))
} else {
let path = Self::installed_bin_path();
if !path.exists() {
Self::install_from_npm().await?;
}
Ok(path)
}
}
/// Return the esbuild binary path if it's already been installed or is on PATH.
/// This is a sync check intended for use after `verify_tooling` has been called.
pub(crate) fn path_if_installed() -> Option<PathBuf> {
let path = Self::installed_bin_path();
if path.exists() {
Some(path)
} else {
which::which("esbuild").ok()
}
}
/// The path where we cache the esbuild binary.
fn installed_bin_path() -> PathBuf {
let dir = Workspace::dioxus_data_dir().join("esbuild");
let name = if cfg!(windows) {
format!("esbuild-{ESBUILD_VERSION}.exe")
} else {
format!("esbuild-{ESBUILD_VERSION}")
};
dir.join(name)
}
/// Download esbuild from the npm registry and extract the binary.
///
/// esbuild publishes platform-specific packages to npm as tar.gz archives.
/// Each archive contains `package/bin/esbuild[.exe]`.
async fn install_from_npm() -> anyhow::Result<()> {
let platform = Self::npm_platform_package()
.ok_or_else(|| anyhow!("No esbuild binary available for this platform"))?;
let url = format!(
"https://registry.npmjs.org/@esbuild/{platform}/-/{platform}-{ESBUILD_VERSION}.tgz"
);
tracing::info!("Installing esbuild v{ESBUILD_VERSION} from {url}...");
let bytes = reqwest::get(&url)
.await
.with_context(|| format!("Failed to download esbuild from {url}"))?
.bytes()
.await
.context("Failed to read esbuild download response")?;
// Extract `package/bin/esbuild[.exe]` from the tar.gz
let binary_data = Self::extract_binary_from_tgz(&bytes)?;
let binary_path = Self::installed_bin_path();
std::fs::create_dir_all(binary_path.parent().unwrap())
.context("Failed to create esbuild cache directory")?;
std::fs::write(&binary_path, &binary_data).context("Failed to write esbuild binary")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = binary_path.metadata()?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&binary_path, perms)?;
}
tracing::info!("esbuild v{ESBUILD_VERSION} installed successfully");
Ok(())
}
/// Extract the esbuild binary from an npm tar.gz package.
///
/// The binary is at `package/bin/esbuild` (or `package/bin/esbuild.exe` on Windows).
fn extract_binary_from_tgz(tgz_bytes: &[u8]) -> anyhow::Result<Vec<u8>> {
let bin_name = if cfg!(windows) {
"esbuild.exe"
} else {
"esbuild"
};
Self::extract_binary_named_from_tgz(tgz_bytes, bin_name)
}
fn extract_binary_named_from_tgz(tgz_bytes: &[u8], bin_name: &str) -> anyhow::Result<Vec<u8>> {
use flate2::read::GzDecoder;
use std::io::Read;
use tar::Archive;
let decoder = GzDecoder::new(tgz_bytes);
let mut archive = Archive::new(decoder);
let expected_paths = [
format!("package/bin/{bin_name}"),
format!("package/{bin_name}"),
];
let mut archive_entries = Vec::new();
for entry in archive.entries().context("Failed to read tar entries")? {
let mut entry = entry.context("Failed to read tar entry")?;
let path = entry.path().context("Failed to read entry path")?;
let path_string = path.to_string_lossy().replace('\\', "/");
if expected_paths.contains(&path_string) {
let mut data = Vec::new();
entry
.read_to_end(&mut data)
.context("Failed to read esbuild binary from archive")?;
return Ok(data);
}
archive_entries.push(path_string);
}
archive_entries.sort();
archive_entries.truncate(10);
anyhow::bail!(
"esbuild binary not found in archive (expected one of {}). Found entries: {}",
expected_paths.join(", "),
archive_entries.join(", ")
);
}
/// Map the host platform to the npm package name for esbuild.
///
/// esbuild publishes per-platform packages under `@esbuild/{name}`:
/// - darwin-arm64, darwin-x64
/// - linux-x64, linux-arm64
/// - win32-x64, win32-arm64
fn npm_platform_package() -> Option<&'static str> {
if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
Some("darwin-arm64")
} else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
Some("darwin-x64")
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
Some("linux-x64")
} else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
Some("linux-arm64")
} else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
Some("win32-x64")
} else if cfg!(all(target_os = "windows", target_arch = "aarch64")) {
Some("win32-arm64")
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::Esbuild;
use flate2::{write::GzEncoder, Compression};
fn tgz_with_entries(entries: &[(&str, &[u8])]) -> Vec<u8> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
{
let mut tar = tar::Builder::new(&mut encoder);
for (path, contents) in entries {
let mut header = tar::Header::new_gnu();
header.set_size(contents.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append_data(&mut header, *path, *contents).unwrap();
}
tar.finish().unwrap();
}
encoder.finish().unwrap()
}
#[test]
fn extracts_binary_from_bin_layout() {
let tgz = tgz_with_entries(&[("package/bin/esbuild.exe", b"windows-binary")]);
let extracted = Esbuild::extract_binary_named_from_tgz(&tgz, "esbuild.exe").unwrap();
assert_eq!(extracted, b"windows-binary");
}
#[test]
fn extracts_binary_from_package_root_layout() {
let tgz = tgz_with_entries(&[("package/esbuild.exe", b"windows-binary")]);
let extracted = Esbuild::extract_binary_named_from_tgz(&tgz, "esbuild.exe").unwrap();
assert_eq!(extracted, b"windows-binary");
}
}
+78 -17
View File
@@ -42,14 +42,14 @@ use futures_util::FutureExt;
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use std::{any::Any, io::Read, str::FromStr, sync::Arc, time::SystemTime}; use std::{any::Any, io::Read, pin::Pin, str::FromStr, sync::Arc, time::SystemTime};
use std::{borrow::Cow, sync::OnceLock}; use std::{borrow::Cow, sync::OnceLock};
use std::{ use std::{
collections::HashMap, collections::HashMap,
env, env,
fmt::{Debug, Display, Write as _}, fmt::{Debug, Display, Write as _},
sync::atomic::{AtomicBool, Ordering}, sync::atomic::{AtomicBool, Ordering},
time::Instant, time::{Duration, Instant},
}; };
use std::{future::Future, panic::AssertUnwindSafe}; use std::{future::Future, panic::AssertUnwindSafe};
use tracing::{field::Visit, Level, Subscriber}; use tracing::{field::Visit, Level, Subscriber};
@@ -139,10 +139,9 @@ impl TraceController {
/// ///
/// We pass the TraceController around the CLI in a few places, namely the serve command so the TUI /// We pass the TraceController around the CLI in a few places, namely the serve command so the TUI
/// can access things like the logs. /// can access things like the logs.
pub async fn main<F>(run_app: impl FnOnce(Commands, Self) -> F) -> StructuredOutput pub async fn main(
where run_app: impl FnOnce(Commands, Self) -> Pin<Box<dyn Future<Output = Result<StructuredOutput>>>>,
F: Future<Output = Result<StructuredOutput>>, ) -> StructuredOutput {
{
let args = Cli::parse(); let args = Cli::parse();
let tui_active = Arc::new(AtomicBool::new(false)); let tui_active = Arc::new(AtomicBool::new(false));
let is_serve_cmd = matches!(args.action, Commands::Serve(_)); let is_serve_cmd = matches!(args.action, Commands::Serve(_));
@@ -178,7 +177,7 @@ impl TraceController {
// We complete filter out a few fields that are not relevant to the user, like `dx_src` and `json` // We complete filter out a few fields that are not relevant to the user, like `dx_src` and `json`
let fmt_layer = tracing_subscriber::fmt::layer() let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(args.verbosity.verbose) .with_target(false)
.fmt_fields( .fmt_fields(
format::debug_fn(move |writer, field, value| { format::debug_fn(move |writer, field, value| {
if field.name() == "json" && !args.verbosity.json_output { if field.name() == "json" && !args.verbosity.json_output {
@@ -521,7 +520,7 @@ impl TraceController {
error_handled, error_handled,
} = event; } = event;
let mut ph_event = posthog_rs::Event::new(action, distinct_id.to_string()); let mut ph_event = PosthogEvent::new(action, distinct_id.to_string());
// The reporter's fields // The reporter's fields
_ = ph_event.insert_prop("is_ci", CliSettings::is_ci()); _ = ph_event.insert_prop("is_ci", CliSettings::is_ci());
@@ -649,7 +648,7 @@ impl TraceController {
reset_cursor(); reset_cursor();
// re-emit any remaining messages in case they're useful. // re-emit any remaining messages in case they're useful.
while let Ok(Some(msg)) = self.tui_rx.lock().await.try_next() { while let Ok(msg) = self.tui_rx.lock().await.try_recv() {
let content = match msg.content { let content = match msg.content {
TraceContent::Text(text) => text, TraceContent::Text(text) => text,
TraceContent::Cargo(msg) => msg.message.to_string(), TraceContent::Cargo(msg) => msg.message.to_string(),
@@ -729,7 +728,7 @@ impl TraceController {
true => { true => {
let mut msgs = self.telemetry_rx.lock().await; let mut msgs = self.telemetry_rx.lock().await;
let request_body = std::iter::from_fn(|| msgs.try_next().ok().flatten()) let request_body = std::iter::from_fn(|| msgs.try_recv().ok())
.filter_map(|msg| serde_json::to_value(msg).ok()) .filter_map(|msg| serde_json::to_value(msg).ok())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -740,8 +739,7 @@ impl TraceController {
false => { false => {
let mut msgs = self.telemetry_rx.lock().await; let mut msgs = self.telemetry_rx.lock().await;
let msg_list = let msg_list = std::iter::from_fn(|| msgs.try_recv().ok()).collect::<Vec<_>>();
std::iter::from_fn(|| msgs.try_next().ok().flatten()).collect::<Vec<_>>();
if !msg_list.is_empty() { if !msg_list.is_empty() {
let dest = Workspace::dioxus_data_dir() let dest = Workspace::dioxus_data_dir()
@@ -1336,10 +1334,43 @@ impl Default for PrettyUptime {
impl FormatTime for PrettyUptime { impl FormatTime for PrettyUptime {
fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result { fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
let e = self.epoch.elapsed(); Self::write_elapsed(self.epoch.elapsed(), w)
write!(w, "{:4}.{:2}s", e.as_secs(), e.subsec_millis())
} }
} }
impl PrettyUptime {
fn write_elapsed(elapsed: Duration, mut w: impl std::fmt::Write) -> std::fmt::Result {
write!(
w,
"{:3}.{:02}s",
elapsed.as_secs(),
elapsed.subsec_millis() / 10
)
}
}
#[cfg(test)]
mod pretty_uptime_tests {
use super::PrettyUptime;
use std::time::Duration;
#[test]
fn pretty_uptime_pads_centiseconds_to_keep_a_stable_width() {
let cases = [
(Duration::from_millis(92_993), " 92.99s"),
(Duration::from_millis(93_085), " 93.08s"),
(Duration::from_millis(93_130), " 93.13s"),
(Duration::from_millis(999_999), "999.99s"),
];
for (elapsed, expected) in cases {
let mut rendered = String::new();
PrettyUptime::write_elapsed(elapsed, &mut rendered).unwrap();
assert_eq!(rendered, expected);
}
}
}
/// Run the provided future and wait for it to complete, handling Ctrl-C gracefully. /// Run the provided future and wait for it to complete, handling Ctrl-C gracefully.
/// ///
/// If ctrl-c is pressed twice, it exits immediately, skipping our telemetry flush. /// If ctrl-c is pressed twice, it exits immediately, skipping our telemetry flush.
@@ -1372,7 +1403,37 @@ async fn run_with_ctrl_c(
/// We only store non-pii information in telemetry to track issues and performance /// We only store non-pii information in telemetry to track issues and performance
/// across the CLI. /// across the CLI.
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Reporter { struct Reporter {
pub session_id: Uuid, session_id: Uuid,
pub distinct_id: Uuid, distinct_id: Uuid,
}
/// A minimal PostHog event, replacing the `posthog-rs` crate dependency.
/// See <https://posthog.com/docs/api/capture>
#[derive(Serialize, Debug)]
struct PosthogEvent {
event: String,
#[serde(rename = "$distinct_id")]
distinct_id: String,
properties: std::collections::HashMap<String, serde_json::Value>,
}
impl PosthogEvent {
fn new(event: impl Into<String>, distinct_id: impl Into<String>) -> Self {
Self {
event: event.into(),
distinct_id: distinct_id.into(),
properties: std::collections::HashMap::new(),
}
}
fn insert_prop<V: Serialize>(
&mut self,
key: impl Into<String>,
value: V,
) -> Result<(), serde_json::Error> {
self.properties
.insert(key.into(), serde_json::to_value(value)?);
Ok(())
}
} }
+25 -20
View File
@@ -5,15 +5,18 @@
#![allow(clippy::doc_overindented_list_items)] #![allow(clippy::doc_overindented_list_items)]
mod build; mod build;
mod bundle_utils; mod bundler;
mod cargo_toml; mod cargo_toml;
mod check;
mod cli; mod cli;
mod config; mod config;
mod devcfg; mod devcfg;
mod dx_build_info; mod dx_build_info;
mod error; mod error;
mod esbuild;
mod fastfs; mod fastfs;
mod logging; mod logging;
mod opt;
mod platform; mod platform;
mod rustcwrapper; mod rustcwrapper;
mod serve; mod serve;
@@ -53,25 +56,27 @@ async fn main() -> ExitCode {
} }
// Run under the tracing collector so we can capture errors/panics. // Run under the tracing collector so we can capture errors/panics.
let result = TraceController::main(|args, tracer| async move { let result = TraceController::main(|args, tracer| {
match args { Box::pin(async move {
Commands::Serve(opts) => opts.serve(&tracer).await, match args {
Commands::Translate(opts) => opts.translate(), Commands::Serve(opts) => opts.serve(&tracer).await,
Commands::New(opts) => opts.create().await, Commands::Translate(opts) => opts.translate(),
Commands::Init(opts) => opts.init().await, Commands::New(opts) => opts.create().await,
Commands::Config(opts) => opts.config().await, Commands::Init(opts) => opts.init().await,
Commands::Autoformat(opts) => opts.autoformat().await, Commands::Config(opts) => opts.config().await,
Commands::Check(opts) => opts.check().await, Commands::Autoformat(opts) => opts.autoformat().await,
Commands::Build(opts) => opts.build().await, Commands::Check(opts) => opts.check().await,
Commands::Bundle(opts) => opts.bundle().await, Commands::Build(opts) => opts.build().await,
Commands::Run(opts) => opts.run().await, Commands::Bundle(opts) => opts.bundle().await,
Commands::SelfUpdate(opts) => opts.self_update().await, Commands::Run(opts) => opts.run().await,
Commands::Tools(BuildTools::BuildAssets(opts)) => opts.run().await, Commands::SelfUpdate(opts) => opts.self_update().await,
Commands::Tools(BuildTools::HotpatchTip(opts)) => opts.run().await, Commands::Tools(BuildTools::BuildAssets(opts)) => opts.run().await,
Commands::Doctor(opts) => opts.doctor().await, Commands::Tools(BuildTools::HotpatchTip(opts)) => opts.run().await,
Commands::Print(opts) => opts.print().await, Commands::Doctor(opts) => opts.doctor().await,
Commands::Components(opts) => opts.run().await, Commands::Print(opts) => opts.print().await,
} Commands::Components(opts) => opts.run().await,
}
})
}); });
// Print the structured output in JSON format for third-party tools to consume. // Print the structured output in JSON format for third-party tools to consume.
File renamed without changes.
@@ -3,7 +3,7 @@ use manganis::{AssetOptions, CssModuleAssetOptions, FolderAssetOptions};
use manganis_core::{AssetVariant, CssAssetOptions, ImageAssetOptions, JsAssetOptions}; use manganis_core::{AssetVariant, CssAssetOptions, ImageAssetOptions, JsAssetOptions};
use std::path::Path; use std::path::Path;
use crate::css::{process_css_module, process_scss}; use crate::opt::css::{process_css_module, process_scss};
use super::{ use super::{
css::process_css, folder::process_folder, image::process_image, js::process_js, css::process_css, folder::process_folder, image::process_image, js::process_js,
@@ -11,12 +11,13 @@ use super::{
}; };
/// Process a specific file asset with the given options reading from the source and writing to the output path /// Process a specific file asset with the given options reading from the source and writing to the output path
pub fn process_file_to( pub(crate) fn process_file_to(
options: &AssetOptions, options: &AssetOptions,
source: &Path, source: &Path,
output_path: &Path, output_path: &Path,
esbuild_path: Option<&Path>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
process_file_to_with_options(options, source, output_path, false) process_file_to_with_options(options, source, output_path, false, esbuild_path)
} }
/// Process a specific file asset with additional options /// Process a specific file asset with additional options
@@ -25,6 +26,7 @@ pub(crate) fn process_file_to_with_options(
source: &Path, source: &Path,
output_path: &Path, output_path: &Path,
in_folder: bool, in_folder: bool,
esbuild_path: Option<&Path>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
// If the file already exists and this is a hashed asset, then we must have a file // If the file already exists and this is a hashed asset, then we must have a file
// with the same hash already. The hash has the file contents and options, so if we // with the same hash already. The hash has the file contents and options, so if we
@@ -61,7 +63,7 @@ pub(crate) fn process_file_to_with_options(
process_scss(options, source, &temp_path)?; process_scss(options, source, &temp_path)?;
} }
ResolvedAssetType::Js(options) => { ResolvedAssetType::Js(options) => {
process_js(options, source, &temp_path, !in_folder)?; process_js(options, source, &temp_path, !in_folder, esbuild_path)?;
} }
ResolvedAssetType::Image(options) => { ResolvedAssetType::Image(options) => {
process_image(options, source, &temp_path)?; process_image(options, source, &temp_path)?;
@@ -70,7 +72,7 @@ pub(crate) fn process_file_to_with_options(
process_json(source, &temp_path)?; process_json(source, &temp_path)?;
} }
ResolvedAssetType::Folder(_) => { ResolvedAssetType::Folder(_) => {
process_folder(source, &temp_path)?; process_folder(source, &temp_path, esbuild_path)?;
} }
ResolvedAssetType::File => { ResolvedAssetType::File => {
let source_file = std::fs::File::open(source)?; let source_file = std::fs::File::open(source)?;
@@ -2,10 +2,14 @@ use std::path::Path;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use crate::file::process_file_to_with_options; use crate::opt::file::process_file_to_with_options;
/// Process a folder, optimizing and copying all assets into the output folder /// Process a folder, optimizing and copying all assets into the output folder
pub fn process_folder(source: &Path, output_folder: &Path) -> anyhow::Result<()> { pub fn process_folder(
source: &Path,
output_folder: &Path,
esbuild_path: Option<&Path>,
) -> anyhow::Result<()> {
// Create the folder // Create the folder
std::fs::create_dir_all(output_folder)?; std::fs::create_dir_all(output_folder)?;
@@ -21,9 +25,9 @@ pub fn process_folder(source: &Path, output_folder: &Path) -> anyhow::Result<()>
let metadata = file.metadata()?; let metadata = file.metadata()?;
let output_path = output_folder.join(file.strip_prefix(source)?); let output_path = output_folder.join(file.strip_prefix(source)?);
if metadata.is_dir() { if metadata.is_dir() {
process_folder(&file, &output_path) process_folder(&file, &output_path, esbuild_path)
} else { } else {
process_file_minimal(&file, &output_path) process_file_minimal(&file, &output_path, esbuild_path)
} }
})?; })?;
@@ -31,12 +35,17 @@ pub fn process_folder(source: &Path, output_folder: &Path) -> anyhow::Result<()>
} }
/// Optimize a file without changing any of its contents significantly (e.g. by changing the extension) /// Optimize a file without changing any of its contents significantly (e.g. by changing the extension)
fn process_file_minimal(input_path: &Path, output_path: &Path) -> anyhow::Result<()> { fn process_file_minimal(
input_path: &Path,
output_path: &Path,
esbuild_path: Option<&Path>,
) -> anyhow::Result<()> {
process_file_to_with_options( process_file_to_with_options(
&manganis_core::AssetOptions::builder().into_asset_options(), &manganis_core::AssetOptions::builder().into_asset_options(),
input_path, input_path,
output_path, output_path,
true, true,
esbuild_path,
)?; )?;
Ok(()) Ok(())
} }
@@ -6,7 +6,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use crate::{ use crate::opt::{
css::hash_scss, css::hash_scss,
file::{resolve_asset_options, ResolvedAssetType}, file::{resolve_asset_options, ResolvedAssetType},
js::hash_js, js::hash_js,
@@ -48,8 +48,8 @@ fn hash_file(options: &AssetOptions, source: &Path) -> anyhow::Result<AssetHash>
let mut hash = std::collections::hash_map::DefaultHasher::new(); let mut hash = std::collections::hash_map::DefaultHasher::new();
options.hash(&mut hash); options.hash(&mut hash);
// Hash the version of CLI opt // Hash the version of the CLI
hash.write(crate::build_info::version().as_bytes()); hash.write(crate::dx_build_info::PKG_VERSION.as_bytes());
hash_file_with_options(options, source, &mut hash, false)?; hash_file_with_options(options, source, &mut hash, false)?;
let hash = hash.finish(); let hash = hash.finish();
@@ -128,7 +128,7 @@ pub(crate) fn hash_file_contents(source: &Path, hasher: &mut impl Hasher) -> any
} }
/// Add a hash to the asset, or log an error if it fails /// Add a hash to the asset, or log an error if it fails
pub fn add_hash_to_asset(asset: &mut BundledAsset) { pub(crate) fn add_hash_to_asset(asset: &mut BundledAsset) {
let source = asset.absolute_source_path(); let source = asset.absolute_source_path();
match AssetHash::hash_file_contents(asset.options(), source) { match AssetHash::hash_file_contents(asset.options(), source) {
Ok(hash) => { Ok(hash) => {
@@ -1,12 +1,8 @@
use std::path::Path;
use anyhow::Context; use anyhow::Context;
use jpg::compress_jpg; use image::{DynamicImage, EncodableLayout};
use manganis_core::{ImageAssetOptions, ImageFormat, ImageSize}; use manganis_core::{ImageAssetOptions, ImageFormat, ImageSize};
use png::compress_png; use std::io::{BufWriter, Write};
use std::path::Path;
mod jpg;
mod png;
pub(crate) fn process_image( pub(crate) fn process_image(
image_options: &ImageAssetOptions, image_options: &ImageAssetOptions,
@@ -71,3 +67,70 @@ pub(crate) fn process_image(
Ok(()) Ok(())
} }
pub(crate) fn compress_png(image: DynamicImage, output_location: &Path) {
// Image loading/saving is outside scope of this library
let width = image.width() as usize;
let height = image.height() as usize;
let bitmap: Vec<_> = image
.into_rgba8()
.pixels()
.map(|px| imagequant::RGBA::new(px[0], px[1], px[2], px[3]))
.collect();
// Configure the library
let mut liq = imagequant::new();
liq.set_speed(5).unwrap();
liq.set_quality(0, 99).unwrap();
// Describe the bitmap
let mut img = liq.new_image(&bitmap[..], width, height, 0.0).unwrap();
// The magic happens in quantize()
let mut res = match liq.quantize(&mut img) {
Ok(res) => res,
Err(err) => panic!("Quantization failed, because: {err:?}"),
};
let (palette, pixels) = res.remapped(&mut img).unwrap();
let file = std::fs::File::create(output_location).unwrap();
let w = &mut BufWriter::new(file);
let mut encoder = png::Encoder::new(w, width as u32, height as u32);
encoder.set_color(png::ColorType::Rgba);
let mut flattened_palette = Vec::new();
let mut alpha_palette = Vec::new();
for px in palette {
flattened_palette.push(px.r);
flattened_palette.push(px.g);
flattened_palette.push(px.b);
alpha_palette.push(px.a);
}
encoder.set_palette(flattened_palette);
encoder.set_trns(alpha_palette);
encoder.set_depth(png::BitDepth::Eight);
encoder.set_color(png::ColorType::Indexed);
encoder.set_compression(png::Compression::Best);
let mut writer = encoder.write_header().unwrap();
writer.write_image_data(&pixels).unwrap();
writer.finish().unwrap();
}
pub(crate) fn compress_jpg(image: DynamicImage, output_location: &Path) -> anyhow::Result<()> {
let mut comp = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_EXT_RGBX);
let width = image.width() as usize;
let height = image.height() as usize;
comp.set_size(width, height);
let mut comp = comp.start_compress(Vec::new())?; // any io::Write will work
comp.write_scanlines(image.to_rgba8().as_bytes())?;
let jpeg_bytes = comp.finish()?;
let file = std::fs::File::create(output_location)?;
let w = &mut BufWriter::new(file);
w.write_all(&jpeg_bytes)?;
Ok(())
}
+87
View File
@@ -0,0 +1,87 @@
use std::path::Path;
use anyhow::Context;
use manganis_core::JsAssetOptions;
use crate::opt::hash::hash_file_contents;
pub(crate) fn process_js(
js_options: &JsAssetOptions,
source: &Path,
output_path: &Path,
bundle: bool,
esbuild_path: Option<&Path>,
) -> anyhow::Result<()> {
let minify = js_options.minified();
let needs_esbuild = minify || bundle;
if needs_esbuild {
if let Some(esbuild) = esbuild_path {
match run_esbuild(esbuild, source, output_path, bundle, minify) {
Ok(()) => return Ok(()),
Err(err) => {
tracing::error!(
"Failed to process JS with esbuild. Falling back to copy: {err}"
);
}
}
} else {
tracing::warn!("esbuild binary path not set. Copying JS without processing.");
}
}
// Fallback: copy unprocessed
let mut source_file = std::fs::File::open(source)?;
let mut writer = std::io::BufWriter::new(std::fs::File::create(output_path)?);
std::io::copy(&mut source_file, &mut writer).with_context(|| {
format!(
"Failed to write JS to output location: {}",
output_path.display()
)
})?;
Ok(())
}
/// Run esbuild to bundle and/or minify a JavaScript file.
fn run_esbuild(
esbuild: &Path,
source: &Path,
output_path: &Path,
bundle: bool,
minify: bool,
) -> anyhow::Result<()> {
let mut cmd = std::process::Command::new(esbuild);
cmd.arg(source);
cmd.arg(format!("--outfile={}", output_path.display()));
cmd.arg("--log-level=warning");
if bundle {
cmd.arg("--bundle");
cmd.arg("--format=esm");
}
if minify {
cmd.arg("--minify");
}
tracing::debug!("Running esbuild: {:?}", cmd);
let output = cmd.output().context("Failed to run esbuild")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("esbuild failed: {stderr}");
}
Ok(())
}
pub(crate) fn hash_js(
_js_options: &JsAssetOptions,
source: &Path,
hasher: &mut impl std::hash::Hasher,
_bundle: bool,
) -> anyhow::Result<()> {
hash_file_contents(source, hasher)
}
File renamed without changes.
@@ -1,13 +1,8 @@
use anyhow::Context;
use manganis::AssetOptions;
use manganis_core::BundledAsset; use manganis_core::BundledAsset;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, HashSet};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
mod build_info;
mod css; mod css;
mod file; mod file;
mod folder; mod folder;
@@ -16,14 +11,14 @@ mod image;
mod js; mod js;
mod json; mod json;
pub use file::process_file_to; pub(crate) use file::process_file_to;
pub use hash::add_hash_to_asset; pub(crate) use hash::add_hash_to_asset;
/// A manifest of all assets collected from dependencies /// A manifest of all assets collected from dependencies
/// ///
/// This will be filled in primarily by incremental compilation artifacts. /// This will be filled in primarily by incremental compilation artifacts.
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
pub struct AssetManifest { pub(crate) struct AssetManifest {
/// Map of bundled asset name to the asset itself /// Map of bundled asset name to the asset itself
assets: BTreeMap<PathBuf, HashSet<BundledAsset>>, assets: BTreeMap<PathBuf, HashSet<BundledAsset>>,
} }
@@ -88,41 +83,4 @@ impl AssetManifest {
.flat_map(|assets| assets.iter()) .flat_map(|assets| assets.iter())
.filter(move |asset| seen.insert(asset.bundled_path())) .filter(move |asset| seen.insert(asset.bundled_path()))
} }
pub fn load_from_file(path: &Path) -> anyhow::Result<Self> {
let src = std::fs::read_to_string(path)?;
serde_json::from_str(&src)
.with_context(|| format!("Failed to parse asset manifest from {path:?}\n{src}"))
}
}
/// Optimize a list of assets in parallel
pub fn optimize_all_assets(
assets_to_transfer: Vec<(PathBuf, PathBuf, AssetOptions)>,
on_optimization_start: impl FnMut(&Path, &Path, &AssetOptions) + Sync + Send,
on_optimization_end: impl FnMut(&Path, &Path, &AssetOptions) + Sync + Send,
) -> anyhow::Result<()> {
let on_optimization_start = Arc::new(RwLock::new(on_optimization_start));
let on_optimization_end = Arc::new(RwLock::new(on_optimization_end));
assets_to_transfer
.par_iter()
.try_for_each(|(from, to, options)| {
{
let mut on_optimization_start = on_optimization_start.write().unwrap();
on_optimization_start(from, to, options);
}
let res = process_file_to(options, from, to);
if let Err(err) = res.as_ref() {
tracing::error!("Failed to copy asset {from:?}: {err}");
}
{
let mut on_optimization_end = on_optimization_end.write().unwrap();
on_optimization_end(from, to, options);
}
res.map(|_| ())
})
} }
+21
View File
@@ -439,6 +439,7 @@ impl Output {
/// Render the current state of everything to the console screen /// Render the current state of everything to the console screen
pub fn render(&mut self, runner: &AppServer, server: &WebServer) { pub fn render(&mut self, runner: &AppServer, server: &WebServer) {
if !self.interactive { if !self.interactive {
self.emit_logs_to_tracing();
return; return;
} }
@@ -1094,11 +1095,31 @@ impl Output {
lines lines
} }
/// Drains the current log queue to tracing so they still get printed even if the output is non-interactive
fn emit_logs_to_tracing(&mut self) {
// In non-interactive mode (CI, piped output), drain pending logs to tracing
// so they still appear in stdout/stderr instead of being silently dropped.
while let Some(log) = self.pending_logs.pop_back() {
let msg = match &log.content {
TraceContent::Text(s) => s.as_str(),
TraceContent::Cargo(d) => d.message.as_str(),
};
match log.level {
Level::ERROR => tracing::error!("{msg}"),
Level::WARN => tracing::warn!("{msg}"),
Level::INFO => tracing::info!("{msg}"),
Level::DEBUG => tracing::debug!("{msg}"),
Level::TRACE => tracing::trace!("{msg}"),
}
}
}
} }
impl std::ops::Drop for Output { impl std::ops::Drop for Output {
fn drop(&mut self) { fn drop(&mut self) {
if !self.interactive { if !self.interactive {
self.emit_logs_to_tracing();
return; return;
} }
+2 -2
View File
@@ -300,7 +300,7 @@ impl AppServer {
let mut changes: Vec<_> = event.into_iter().collect(); let mut changes: Vec<_> = event.into_iter().collect();
// Dequeue in bulk if we can, we might've received a lot of events in one go // Dequeue in bulk if we can, we might've received a lot of events in one go
while let Some(event) = self.watcher_rx.try_next().ok().flatten() { while let Ok(event) = self.watcher_rx.try_recv() {
changes.push(event); changes.push(event);
} }
@@ -1313,7 +1313,7 @@ impl AppServer {
let depth = relative.components().count(); let depth = relative.components().count();
let is_better = best_match let is_better = best_match
.as_ref() .as_ref()
.map_or(true, |(_, best_depth)| depth < *best_depth); .is_none_or(|(_, best_depth)| depth < *best_depth);
if is_better { if is_better {
best_match = Some((krate.name.replace('-', "_"), depth)); best_match = Some((krate.name.replace('-', "_"), depth));
} }
+1 -1
View File
@@ -26,7 +26,7 @@ use futures_util::{
StreamExt, StreamExt,
}; };
use hyper::HeaderMap; use hyper::HeaderMap;
use rustls::crypto::{aws_lc_rs::default_provider, CryptoProvider}; use rustls::crypto::{ring::default_provider, CryptoProvider};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
convert::Infallible, convert::Infallible,
+5 -5
View File
@@ -135,15 +135,15 @@ async fn find_latest_wasm_opt_download_url() -> anyhow::Result<String> {
// Find the platform identifier based on the current OS and architecture // Find the platform identifier based on the current OS and architecture
// hardcoded for now to get around github api rate limits // hardcoded for now to get around github api rate limits
if cfg!(all(target_os = "windows", target_arch = "x86_64")) { if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-x86_64-windows.tar.gz".to_string()); return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-x86_64-windows.tar.gz".to_string());
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) { } else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-x86_64-linux.tar.gz".to_string()); return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-x86_64-linux.tar.gz".to_string());
} else if cfg!(all(target_os = "linux", target_arch = "aarch64")) { } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-aarch64-linux.tar.gz".to_string()); return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-aarch64-linux.tar.gz".to_string());
} else if cfg!(all(target_os = "macos", target_arch = "x86_64")) { } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-x86_64-macos.tar.gz".to_string()); return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-x86_64-macos.tar.gz".to_string());
} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) { } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-arm64-macos.tar.gz".to_string()); return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-arm64-macos.tar.gz".to_string());
}; };
let url = "https://api.github.com/repos/WebAssembly/binaryen/releases/latest"; let url = "https://api.github.com/repos/WebAssembly/binaryen/releases/latest";
+1 -1
View File
@@ -479,7 +479,7 @@ impl VirtualDom {
/// Queue any pending events /// Queue any pending events
fn queue_events(&mut self) { fn queue_events(&mut self) {
// Prevent a task from deadlocking the runtime by repeatedly queueing itself // Prevent a task from deadlocking the runtime by repeatedly queueing itself
while let Ok(Some(msg)) = self.rx.try_next() { while let Ok(msg) = self.rx.try_recv() {
match msg { match msg {
SchedulerMsg::Immediate(id) => self.mark_dirty(id), SchedulerMsg::Immediate(id) => self.mark_dirty(id),
SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)), SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)),
+9 -2
View File
@@ -131,9 +131,16 @@ impl Config {
self self
} }
/// set the directory where data will be stored in release mode. /// Set the directory where WebView2 stores its user data (cookies, cache, IndexedDB, etc.).
/// ///
/// > Note: This **must** be set when bundling on Windows. /// If not set, Dioxus automatically chooses a sensible default:
/// - **Windows:** `%LOCALAPPDATA%/<exe_name>` — this avoids the WebView2 default of
/// placing data next to the executable, which fails in read-only locations like
/// `Program Files` or on certain drive types (e.g. ReFS dev drives).
/// - **macOS/Linux:** managed by WebKit automatically.
///
/// You typically only need this if you want multiple apps to share a WebView2 profile
/// or need the data in a specific location for compliance/IT policy reasons.
pub fn with_data_directory(mut self, path: impl Into<PathBuf>) -> Self { pub fn with_data_directory(mut self, path: impl Into<PathBuf>) -> Self {
self.data_dir = Some(path.into()); self.data_dir = Some(path.into());
self self
+1
View File
@@ -231,6 +231,7 @@ impl EditWebsocket {
let hex_encoded_server_key = encode_key_string(&current_server_location.server_key); let hex_encoded_server_key = encode_key_string(&current_server_location.server_key);
let mut location = None; let mut location = None;
#[allow(clippy::result_large_err)]
let on_request = |req: &Request, res| { let on_request = |req: &Request, res| {
// Try to parse the webview id and key from the path // Try to parse the webview id and key from the path
let path = req.uri().path(); let path = req.uri().path();
+1
View File
@@ -3,6 +3,7 @@
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![deny(missing_docs)] #![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(unexpected_cfgs)]
mod android_sync_lock; mod android_sync_lock;
mod app; mod app;
+13 -2
View File
@@ -260,12 +260,23 @@ impl WebviewInstance {
unsafe { unsafe {
let window: id = window.ns_window() as id; let window: id = window.ns_window() as id;
#[allow(unexpected_cfgs)]
let _: () = msg_send![window, setCollectionBehavior: NSWindowCollectionBehavior::NSWindowCollectionBehaviorManaged]; let _: () = msg_send![window, setCollectionBehavior: NSWindowCollectionBehavior::NSWindowCollectionBehaviorManaged];
} }
} }
let mut web_context = WebContext::new(cfg.data_dir.clone()); let mut web_context = WebContext::new(cfg.data_dir.clone().or_else(|| {
// On Windows, WebView2 defaults to storing its data next to the executable.
// This fails on certain drives (e.g. ReFS dev drives, Program Files) where the
// directory may not be writable. Fall back to %LOCALAPPDATA%/<exe_name> automatically.
if cfg!(windows) {
let exe = std::env::current_exe().ok()?;
let name = exe.file_stem()?.to_str()?;
let local_app_data = std::env::var("LOCALAPPDATA").ok()?;
Some(std::path::PathBuf::from(local_app_data).join(name))
} else {
None
}
}));
let edit_queue = shared.websocket.create_queue(); let edit_queue = shared.websocket.create_queue();
let asset_handlers = AssetHandlerRegistry::new(); let asset_handlers = AssetHandlerRegistry::new();
let edits = WebviewEdits::new(dom.runtime(), edit_queue.clone()); let edits = WebviewEdits::new(dom.runtime(), edit_queue.clone());
+1
View File
@@ -94,6 +94,7 @@ pub enum BuildStage {
current: usize, current: usize,
total: usize, total: usize,
krate: String, krate: String,
fresh: bool,
}, },
RunningBindgen, RunningBindgen,
SplittingBundle, SplittingBundle,
+84 -223
View File
@@ -1,251 +1,112 @@
// @ts-check // @ts-check
const { defineConfig, devices } = require("@playwright/test"); const { defineConfig, devices } = require("@playwright/test");
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path"); const path = require("path");
/** // Copy a directory to a temp location for tests that modify source files (hot-patch tests).
* Read environment variables from file. // Done in JS so the webServer command is a simple `dx serve` with a `cwd`, which keeps
* https://github.com/motdotla/dotenv // stdout/stderr piping clean on both Unix and Windows cmd.exe.
*/ function copyToTemp(src, dest) {
// require('dotenv').config(); const absSrc = path.resolve(__dirname, src);
const absDest = path.resolve(__dirname, dest);
fs.rmSync(absDest, { recursive: true, force: true });
fs.cpSync(absSrc, absDest, { recursive: true });
}
let webServer = []; const repoRoot = path.resolve(__dirname, "..", "..");
let grep = undefined; const dx = path.join(repoRoot, "target", "release", process.platform === "win32" ? "dx.exe" : "dx");
let grepInvert = undefined;
const ALL_SERVERS = [
{
specs: ["liveview.spec.js"], port: 3030,
command: "cargo run --package dioxus-playwright-liveview-test --bin dioxus-playwright-liveview-test",
env: { CARGO_TERM_PROGRESS_WHEN: "never" }
},
{ specs: ["web.spec.js"], port: 9990, cwd: "web", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 9990` },
{ specs: ["web-routing.spec.js"], port: 2020, cwd: "web-routing", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 2020` },
{ specs: ["web-hash-routing.spec.js"], port: 2021, cwd: "web-hash-routing", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 2021` },
{ specs: ["fullstack.spec.js"], port: 3333, cwd: "fullstack", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 3333 --release` },
{ specs: ["fullstack-errors.spec.js"], port: 3232, cwd: "fullstack-errors", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 3232` },
{ specs: ["fullstack-mounted.spec.js"], port: 7777, cwd: "fullstack-mounted", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 7777` },
{ specs: ["fullstack-routing.spec.js"], port: 8888, cwd: "fullstack-routing", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 8888` },
{ specs: ["fullstack-spread.spec.js"], port: 7980, cwd: "fullstack-spread", command: `${dx} run --verbose --force-sequential --web --addr 127.0.0.1 --port 7980` },
{ specs: ["fullstack-hydration-order.spec.js"], port: 7979, cwd: "fullstack-hydration-order", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 7979` },
{ specs: ["fullstack-error-codes.spec.js"], port: 8124, cwd: "fullstack-error-codes", command: `${dx} run --force-sequential --addr 127.0.0.1 --port 8124` },
{ specs: ["nested-suspense.spec.js", "nested-suspense-no-js.spec.js"], port: 5050, cwd: "nested-suspense", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 5050` },
{ specs: ["nested-suspense-ssg.spec.js"], port: 6060, cwd: "nested-suspense", command: `${dx} run --bin nested-suspense-ssg --force-sequential --web --ssg --addr 127.0.0.1 --port 6060` },
{ specs: ["suspense-carousel.spec.js"], port: 4040, cwd: "suspense-carousel", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 4040` },
{ specs: ["cli-optimization.spec.js"], port: 8989, cwd: "cli-optimization", command: `${dx} run --addr 127.0.0.1 --port 8989` },
{ specs: ["wasm-split.spec.js"], port: 8001, cwd: "wasm-split-harness", command: `${dx} run --bin wasm-split-harness --web --addr 127.0.0.1 --port 8001 --wasm-split --profile wasm-split-release` },
{ specs: ["default-features-disabled.spec.js"], port: 8002, cwd: "default-features-disabled", command: `${dx} run --force-sequential --addr 127.0.0.1 --port 8002` },
{ specs: ["web-patch.spec.js"], port: 9980, cwd: "web-hot-patch-temp", setup: () => copyToTemp("web-hot-patch", "web-hot-patch-temp"), command: `${dx} serve --verbose --force-sequential --web --addr 127.0.0.1 --port 9980 --hot-patch --exit-on-error` },
{ specs: ["web-patch-fullstack.spec.js"], port: 9981, cwd: "web-hot-patch-fullstack-temp", setup: () => copyToTemp("web-hot-patch-fullstack", "web-hot-patch-fullstack-temp"), command: `${dx} serve --verbose --force-sequential --web --addr 127.0.0.1 --port 9981 --hot-patch --exit-on-error` },
];
if (process.platform === "win32") { if (process.platform === "win32") {
webServer = [ ALL_SERVERS.push({ specs: ["windows.spec.js"], port: 8787, cwd: "windows-headless", command: `${dx} run --force-sequential` });
{ ALL_SERVERS.push({ specs: ["windows-hotpatch-fullstack.spec.js"], port: 8788, cwd: "windows-hotpatch-fullstack-temp", setup: () => copyToTemp("windows-hotpatch-fullstack", "windows-hotpatch-fullstack-temp"), command: `${dx} serve --verbose --force-sequential --hot-patch --exit-on-error` });
cwd: path.join(process.cwd(), "windows-headless"), }
command:
"cargo run --package dioxus-cli --release -- run --force-sequential", // Determine which servers to start based on spec files in argv and platform.
port: 8787, // On Windows only windows.spec.js runs (grep filter below), so don't bother
timeout: 50 * 60 * 1000, // starting servers for tests that will never execute — they just crash and
reuseExistingServer: !process.env.CI, // pollute the logs.
stdout: "pipe", const specArgs = process.argv.filter((a) => a.endsWith(".spec.js")).map((a) => path.basename(a));
}, const isWindows = process.platform === "win32";
]; const activeServers = specArgs.length > 0
grep = /windows/; ? ALL_SERVERS.filter((s) => s.specs.some((spec) => specArgs.includes(spec)))
} else { : isWindows
webServer = [ ? ALL_SERVERS.filter((s) => s.specs.some((spec) => spec.includes("windows")))
{ : ALL_SERVERS.filter((s) => !s.specs.some((spec) => spec.includes("windows")));
command:
"cargo run --package dioxus-playwright-liveview-test --bin dioxus-playwright-liveview-test", // Run any setup functions (e.g. copying source to temp dirs for hot-patch tests)
port: 3030, // Only happens on initialize
timeout: 50 * 60 * 1000, //
reuseExistingServer: !process.env.CI, // Build dx once: main process builds and sets env var; workers inherit it and skip.
stdout: "pipe", // todo: implement proper fixtures https://www.youtube.com/watch?v=3i6cJUFO_m4
}, if (!process.env._DX_BUILT) {
{ for (const s of activeServers) {
cwd: path.join(process.cwd(), "web"), if (s.setup) s.setup();
command: }
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 9990',
port: 9990, execSync("cargo build --package dioxus-cli --release", {
timeout: 50 * 60 * 1000, cwd: repoRoot,
reuseExistingServer: !process.env.CI, env: { ...process.env, CARGO_TERM_PROGRESS_WHEN: "never" },
stdout: "pipe", stdio: "inherit",
}, });
{
cwd: path.join(process.cwd(), "web-routing"),
command:
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 2020',
port: 2020,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "web-hash-routing"),
command:
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 2021',
port: 2021,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "fullstack"),
command:
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 3333 --release',
port: 3333,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "fullstack-errors"),
command:
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 3232',
port: 3232,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "fullstack-mounted"),
command:
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 7777',
port: 7777,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "fullstack-spread"),
command:
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 7980',
port: 7980,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "fullstack-routing"),
command:
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 8888',
port: 8888,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "suspense-carousel"),
command:
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 4040',
port: 4040,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "nested-suspense"),
command:
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 5050',
port: 5050,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "nested-suspense"),
command:
'cargo run --package dioxus-cli --release -- run --bin nested-suspense-ssg --force-sequential --web --ssg --addr "127.0.0.1" --port 6060',
port: 6060,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "fullstack-hydration-order"),
command:
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 7979',
port: 7979,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "cli-optimization"),
command:
'cargo run --package dioxus-cli --release -- run --addr "127.0.0.1" --port 8989',
port: 8989,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "wasm-split-harness"),
command:
'cargo run --package dioxus-cli --release -- run --bin wasm-split-harness --web --addr "127.0.0.1" --port 8001 --wasm-split --profile wasm-split-release',
port: 8001,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "default-features-disabled"),
command:
'cargo run --package dioxus-cli --release -- run --force-sequential --addr "127.0.0.1" --port 8002',
port: 8002,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "barebones-template"),
command:
'cargo run --package dioxus-cli --release -- run --force-sequential --addr "127.0.0.1" --port 8123',
port: 8123,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
command:
'rm -rf ./web-hot-patch-temp && cp -r ./web-hot-patch ./web-hot-patch-temp && cd web-hot-patch-temp && cargo run --manifest-path ../../cli/Cargo.toml --release -- serve --verbose --force-sequential --web --addr "127.0.0.1" --port 9980 --hot-patch --exit-on-error',
port: 9980,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
command:
'rm -rf ./web-hot-patch-fullstack-temp && cp -r ./web-hot-patch-fullstack ./web-hot-patch-fullstack-temp && cd web-hot-patch-fullstack-temp && cargo run --manifest-path ../../cli/Cargo.toml --release -- serve --verbose --force-sequential --web --addr "127.0.0.1" --port 9981 --hot-patch --exit-on-error',
port: 9981,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "fullstack-error-codes"),
command:
'cargo run --package dioxus-cli --release -- run --force-sequential --addr "127.0.0.1" --port 8124',
port: 8124,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
];
grepInvert = /windows/; process.env._DX_BUILT = "1";
} }
/**
* @see https://playwright.dev/docs/test-configuration
*/
module.exports = defineConfig({ module.exports = defineConfig({
testDir: ".", testDir: ".",
/* Run tests in files in parallel */
fullyParallel: true, fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html", reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "retain-on-failure", trace: "retain-on-failure",
// Increase the timeout for navigations to give dx time to build the project
navigationTimeout: 50 * 60 * 1000, navigationTimeout: 50 * 60 * 1000,
}, },
timeout: 50 * 60 * 1000, timeout: 50 * 60 * 1000,
webServer: activeServers.map((s) => ({
/* Configure projects for major browsers */ command: s.command,
port: s.port,
cwd: s.cwd ? path.join(__dirname, s.cwd) : __dirname,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
stderr: "pipe",
env: s.env,
})),
projects: [ projects: [
{ {
name: "chromium", name: "chromium",
grep, grep: process.platform === "win32" ? /windows/ : undefined,
grepInvert, grepInvert: process.platform !== "win32" ? /windows/ : undefined,
use: { ...devices["Desktop Chrome"] }, use: { ...devices["Desktop Chrome"] },
}, },
], ],
/* Run your local dev server before starting the tests */
webServer,
}); });
@@ -8,7 +8,7 @@ publish = false
[dependencies] [dependencies]
dioxus = { path = "../../dioxus", features = ["web"] } dioxus = { path = "../../dioxus", features = ["web"] }
wasm-bindgen = { version = "0.2.100" } wasm-bindgen = { version = "0.2.114" }
web-sys = { version = "*", features = ["MouseEvent"] } web-sys = { version = "*", features = ["MouseEvent"] }
[workspace] [workspace]
@@ -0,0 +1,78 @@
// @ts-check
const { test: base, expect } = require("@playwright/test");
const fs = require("fs");
const test = base.extend({
browser: async ({ playwright }, use) => {
const browser = await playwright.chromium.connectOverCDP("http://127.0.0.1:8788");
await use(browser);
await browser.close();
},
context: async ({ browser }, use) => {
const context = browser.contexts()[0];
await use(context);
},
page: async ({ context }, use) => {
const page = context.pages()[0];
await use(page);
},
});
const hotPatchTimeout = { timeout: 1000 * 60 * 2 };
test("windows fullstack hotpatch", async ({ page }) => {
const mainPath = "windows-hotpatch-fullstack-temp/src/main.rs";
const stylePath = "windows-hotpatch-fullstack-temp/assets/style.css";
let mainContent = fs.readFileSync(mainPath, "utf8");
let styleContent = fs.readFileSync(stylePath, "utf8");
// Reset any changes from prior runs.
mainContent = mainContent.replace(/Ok\(\s*2\s*\)/g, "Ok(1)");
mainContent = mainContent.replace("Click button! Count:", "Click me! Count:");
fs.writeFileSync(mainPath, mainContent);
styleContent = styleContent.replace("background-color: blue;", "background-color: red;");
fs.writeFileSync(stylePath, styleContent);
// ** Verify the initial fat build is working **
const main = page.locator("#main");
await expect(main).toContainText("Click me! Count: 0");
// Click and verify the server function works.
const button = page.locator("button#increment-button");
await button.click();
await page.waitForTimeout(1000);
await expect(main).toContainText("Click me! Count: 1");
// Verify initial CSS is applied.
await expect(main).toHaveCSS("background-color", "rgb(255, 0, 0)");
// ** Hot patch: change server fn return value and button text **
const updatedContent = mainContent
.replace(/Ok\(\s*1\s*\)/g, "Ok(2)")
.replace("Click me! Count:", "Click button! Count:");
fs.writeFileSync(mainPath, updatedContent);
// Wait for the hot patch to apply (text should update).
await expect(main).toContainText("Click button! Count: 1", hotPatchTimeout);
// Click and verify the new increment amount from the patched server fn.
await button.click();
await page.waitForTimeout(5000);
await expect(main).toContainText("Click button! Count: 3");
// ** Hot patch CSS **
const updatedStyle = styleContent.replace(
"background-color: red;",
"background-color: blue;"
);
fs.writeFileSync(stylePath, updatedStyle);
await page.waitForTimeout(1000);
// Wait for the CSS hot patch to apply.
await expect(main).toHaveCSS(
"background-color",
"rgb(0, 0, 255)",
hotPatchTimeout
);
});
@@ -0,0 +1,14 @@
[package]
name = "windows-hotpatch-fullstack"
edition = "2021"
version = "0.0.1"
publish = false
[dependencies]
dioxus = { path = "../../dioxus", features = ["fullstack"] }
[features]
desktop = ["dioxus/desktop"]
server = ["dioxus/server"]
[workspace]
@@ -0,0 +1,3 @@
#main {
background-color: red;
}
@@ -0,0 +1,38 @@
use dioxus::prelude::*;
const CSS: Asset = asset!("/assets/style.css");
fn app() -> Element {
let mut num = use_signal(|| 0);
rsx! {
document::Link {
href: CSS,
rel: "stylesheet",
}
button {
id: "increment-button",
onclick: move |_| async move {
let increment_amount = get_count().await.unwrap();
*num.write() += increment_amount;
},
"Click me! Count: {num}"
}
}
}
#[post("/api/get_count")]
async fn get_count() -> Result<i32> {
Ok(1)
}
fn main() {
let builder = dioxus::LaunchBuilder::new();
#[cfg(feature = "desktop")]
let builder = builder.with_cfg(
dioxus::desktop::Config::new().with_windows_browser_args("--remote-debugging-port=8788"),
);
builder.launch(app);
}
+1 -1
View File
@@ -76,7 +76,7 @@ impl<T> Memo<T> {
spawn_isomorphic(async move { spawn_isomorphic(async move {
while rx.next().await.is_some() { while rx.next().await.is_some() {
// Remove any pending updates // Remove any pending updates
while rx.try_next().is_ok() {} while rx.try_recv().is_ok() {}
memo.recompute(); memo.recompute();
} }
}); });
+4 -3
View File
@@ -17,9 +17,9 @@ pub(crate) fn extend_store(args: ExtendArgs, mut input: ItemImpl) -> syn::Result
let visibility = args let visibility = args
.visibility .visibility
.unwrap_or_else(|| syn::Visibility::Inherited); .unwrap_or_else(|| syn::Visibility::Inherited);
if input.trait_.is_some() { if let Some(input_trait) = input.trait_.as_ref() {
return Err(syn::Error::new_spanned( return Err(syn::Error::new_spanned(
input.trait_.unwrap().1, input_trait.1.clone(),
"The `store` attribute can only be used on `impl Store<T> { ... }` blocks, not trait implementations.", "The `store` attribute can only be used on `impl Store<T> { ... }` blocks, not trait implementations.",
)); ));
} }
@@ -86,7 +86,8 @@ pub(crate) fn extend_store(args: ExtendArgs, mut input: ItemImpl) -> syn::Result
let trait_definition = impl_to_trait_body(&extension_name, &input, &visibility)?; let trait_definition = impl_to_trait_body(&extension_name, &input, &visibility)?;
// Reformat the type to be generic over the lens // Reformat the type to be generic over the lens
input.self_ty = Box::new(parse_quote!(#store_path<#item, #lens_generic>)); input.self_ty = parse_quote!(#store_path<#item, #lens_generic>);
// Change the standalone impl block to a trait impl block // Change the standalone impl block to a trait impl block
let (_, trait_generics, _) = input.generics.split_for_impl(); let (_, trait_generics, _) = input.generics.split_for_impl();
input.trait_ = Some(( input.trait_ = Some((