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
#
# 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:
push:
branches:
- main
paths:
- packages/**
- examples/**
- docs/guide/**
- src/**
- .github/**
- lib.rs
- Cargo.toml
- Makefile.toml
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches:
- main
paths:
- packages/**
- examples/**
- src/**
- .github/**
- lib.rs
- Cargo.toml
# workflow_dispatch:
concurrency:
@@ -38,110 +23,113 @@ concurrency:
env:
CARGO_TERM_COLOR: always
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_nightly: nightly-2025-10-05
jobs:
check-msrv:
if: github.event.pull_request.draft == false
name: Check MSRV
runs-on: blacksmith-4vcpu-ubuntu-2404
# ---------------------------------------------------------------------------
# Path gate — determines whether Rust jobs should run
# ---------------------------------------------------------------------------
check-paths:
runs-on: warp-ubuntu-latest-x64-4x
outputs:
rust: ${{ steps.filter.outputs.rust }}
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@1.88.0
- uses: Swatinem/rust-cache@v2
- uses: dorny/paths-filter@v3
id: filter
with:
cache-all-crates: "true"
# https://github.com/foresterre/cargo-msrv/blob/4345edfe3f4fc91cc8ae6c7d6804c0748fae92ae/.github/workflows/msrv.yml
- name: install_cargo_msrv
run: cargo install cargo-msrv --all-features --version 0.16.3 --locked
- name: version_of_cargo_msrv
run: cargo msrv --version
- name: run_cargo_msrv
run: cargo msrv --output-format json verify -- cargo check
- name: run_cargo_msrv_on_verify_failure
if: ${{ failure() }}
run: cargo msrv --output-format json -- cargo check
filters: |
rust:
- 'packages/**'
- 'examples/**'
- 'docs/guide/**'
- 'src/**'
- '.github/**'
- 'lib.rs'
- 'Cargo.toml'
- 'Makefile.toml'
test:
# ---------------------------------------------------------------------------
# Language linting — runs on every push
# ---------------------------------------------------------------------------
typos:
if: github.event.pull_request.draft == false
name: Test Suite
runs-on: blacksmith-4vcpu-ubuntu-2404
name: "Lint | Typos"
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:
- 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
- uses: dtolnay/rust-toolchain@1.94.0
with:
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
with:
cache-all-crates: "true"
- uses: browser-actions/setup-firefox@latest
- run: cargo test --lib --bins --tests --examples --workspace --exclude dioxus-desktop
release-test:
if: github.event.pull_request.draft == false
name: Test Suite with Optimizations
runs-on: blacksmith-4vcpu-ubuntu-2404
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"
- 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
cache-workspace-crates: "true"
cache-provider: "warpbuild"
# save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Rustfmt
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --workspace --examples --tests --all-features --all-targets -- -D warnings
- name: Check schema
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: |
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."
exit 1
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:
if: github.event.pull_request.draft == false
name: Docs
runs-on: blacksmith-4vcpu-ubuntu-2404
needs: check-paths
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
name: "Docs | Build"
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v5
- uses: awalsh128/cache-apt-pkgs-action@latest
@@ -155,6 +143,9 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
cache-workspace-crates: "true"
cache-provider: "warpbuild"
# save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "doc --lib --all-features"
run: |
cargo doc --workspace --no-deps --all-features --document-private-items
@@ -162,9 +153,10 @@ jobs:
RUSTDOCFLAGS: -Dwarnings --document-private-items
test-docs:
if: github.event.pull_request.draft == false
name: Test Docs
runs-on: blacksmith-4vcpu-ubuntu-2404
needs: check-paths
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
name: "Docs | Test"
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v5
- uses: awalsh128/cache-apt-pkgs-action@latest
@@ -178,83 +170,213 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
cache-workspace-crates: "true"
cache-provider: "warpbuild"
# save-if: ${{ github.ref == 'refs/heads/main' }}
- name: "doc --lib --all-features"
run: |
cargo test --doc --workspace --all-features
check:
if: github.event.pull_request.draft == false
name: Check
runs-on: blacksmith-4vcpu-ubuntu-2404
bundle-linux-smoke:
needs: check-paths
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
name: "Bundle | Linux"
runs-on: warp-ubuntu-latest-x64-4x
timeout-minutes: 45
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
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
- uses: dtolnay/rust-toolchain@1.88.0
- uses: Swatinem/rust-cache@v2
with:
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:
if: github.event.pull_request.draft == false
name: Clippy
runs-on: blacksmith-4vcpu-ubuntu-2404
bundle-macos-smoke:
needs: check-paths
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
name: "Bundle | macOS"
runs-on: warp-macos-latest-arm64-6x
timeout-minutes: 45
steps:
- uses: actions/checkout@v5
- uses: awalsh128/cache-apt-pkgs-action@latest
- uses: dtolnay/rust-toolchain@1.88.0
- uses: Swatinem/rust-cache@v2
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.90.0
cache-all-crates: "true"
cache-workspace-crates: "true"
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:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
key: "windows-bundle-${{ runner.os }}"
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:
if: github.event.pull_request.draft == false
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [blacksmith-4vcpu-ubuntu-2404, macos-latest]
windows:
needs: check-paths
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
name: "Playwright | Windows"
runs-on: warp-windows-2025-x64-8x
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- uses: nixbuild/nix-quick-install-action@master
- uses: nix-community/cache-nix-action@main
- 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
- uses: actions/setup-node@v4
with:
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
- name: Install omnix
run: nix --accept-flake-config profile install "github:juspay/omnix"
- name: Build all flake outputs
run: om ci
- name: Ensure devShell has all build deps
run: nix develop -c cargo build -p dioxus-cli --features no-downloads
node-version: 24
- name: Install Rust
uses: dtolnay/rust-toolchain@1.88.0
with:
targets: wasm32-unknown-unknown
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
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:
if: github.event.pull_request.draft == false
name: Playwright Tests
needs: check-paths
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
name: "Playwright | ${{ matrix.name }}"
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
matrix:
os: [blacksmith-4vcpu-windows-2025, blacksmith-8vcpu-ubuntu-2404]
platform:
- { toolchain: beta }
# - { toolchain: 1.86.0 }
include:
- { os: warp-ubuntu-latest-x64-8x, name: "Linux (stable)", toolchain: "1.88.0" }
- { os: warp-ubuntu-latest-x64-8x, name: "Linux (beta)", toolchain: beta }
steps:
# Do our best to cache the toolchain and node install steps
- uses: actions/checkout@v5
- 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: 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:
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
@@ -264,15 +386,17 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.platform.toolchain }}
toolchain: ${{ matrix.toolchain }}
targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
key: "playwright-${{ matrix.platform.toolchain }}-${{ runner.os }}"
key: "playwright-${{ matrix.toolchain }}-${{ runner.os }}"
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
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: |
rm -rf ./target/dx
- name: Playwright
@@ -288,13 +412,67 @@ jobs:
- uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report-${{ matrix.platform.toolchain }}-${{ runner.os }}
name: playwright-report-${{ matrix.toolchain }}-${{ runner.os }}
path: ./packages/playwright-tests/playwright-report/
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:
needs: check-paths
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:
RUST_CARGO_COMMAND: ${{ matrix.platform.cross == true && 'cross' || 'cargo' }}
strategy:
@@ -302,7 +480,7 @@ jobs:
platform:
- {
target: aarch64-apple-darwin,
os: macos-latest,
os: warp-macos-latest-arm64-6x,
toolchain: "1.88.0",
cross: false,
command: "test",
@@ -311,7 +489,7 @@ jobs:
}
- {
target: aarch64-apple-ios,
os: macos-latest,
os: warp-macos-latest-arm64-6x,
toolchain: "1.88.0",
cross: false,
command: "build",
@@ -320,7 +498,7 @@ jobs:
}
- {
target: aarch64-unknown-linux-gnu,
os: ubuntu-24.04-arm,
os: warp-ubuntu-latest-arm64-4x,
toolchain: "1.88.0",
cross: false,
command: "build",
@@ -329,14 +507,14 @@ jobs:
}
- {
target: aarch64-linux-android,
os: blacksmith-4vcpu-ubuntu-2404,
os: warp-ubuntu-latest-x64-4x,
toolchain: "1.88.0",
cross: true,
command: "build",
args: "--package dioxus-desktop",
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,
# os: ubuntu-24.04,
@@ -350,15 +528,31 @@ jobs:
steps:
- uses: actions/checkout@v5
- 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: 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:
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
- 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
uses: dtolnay/rust-toolchain@master
with:
@@ -366,7 +560,7 @@ jobs:
targets: ${{ matrix.platform.target }}
components: rustfmt
- name: Install nasm for windows (tls)
- name: Install nasm for windows (tls)1
if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }}
uses: ilammy/setup-nasm@v1
@@ -378,33 +572,45 @@ jobs:
with:
key: "matrix-${{ matrix.platform.target }}"
cache-all-crates: "true"
cache-workspace-crates: "true"
cache-provider: "warpbuild"
# save-if: ${{ github.ref == 'refs/heads/main' }}
- name: test
run: |
${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}
# borrowed from uv
# https://raw.githubusercontent.com/astral-sh/uv/refs/heads/main/.github/workflows/ci.yml
cargo-test-windows:
if: github.event.pull_request.draft == false
runs-on:
labels: "blacksmith-4vcpu-windows-2025"
name: "cargo test | windows"
nix:
needs: check-paths
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
name: "Nix | ${{ matrix.name }}"
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- { os: warp-ubuntu-latest-x64-8x, name: Linux }
- { os: warp-macos-latest-arm64-6x, name: macOS }
steps:
- 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:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: ${{ env.UV_WORKSPACE }}
cache-all-crates: "true"
- name: "Install Rust toolchain"
working-directory: ${{ env.UV_WORKSPACE }}
run: rustup show
- name: "Cargo test"
working-directory: ${{ env.UV_WORKSPACE }}
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.nix', 'flake.lock') }}-${{ hashFiles('Cargo.lock') }}
restore-prefixes-first-match: |
nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.nix', 'flake.lock') }}-
nix-${{ runner.os }}-${{ runner.arch }}-
backend: warpbuild
- name: Build dioxus-cli from the flake
run: nix build .#dioxus-cli
- name: Smoke test devShell
shell: bash
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
backport:
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
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
docs:
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: warp-ubuntu-latest-x64-4x
permissions:
contents: write
steps:
@@ -47,6 +47,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
cache-provider: "warpbuild"
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo doc
+1 -1
View File
@@ -22,7 +22,7 @@ permissions:
jobs:
promote:
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v5
- name: Publish the next pre-release
+12 -2
View File
@@ -84,11 +84,21 @@ jobs:
uses: ./.github/actions/free-disk-space
- uses: awalsh128/cache-apt-pkgs-action@latest
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
if: ${{ matrix.platform.os == 'ubuntu-24.04' }}
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
- 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
uses: dtolnay/rust-toolchain@master
with:
+69 -21
View File
@@ -1,26 +1,74 @@
# This creates a 20GB dev drive, and exports all required environment
# variables so that rustup, uv and others all use the dev drive as much
# as possible.
$Volume = New-VHD -Path C:/uv_dev_drive.vhdx -SizeBytes 20GB |
Mount-VHD -Passthru |
Initialize-Disk -Passthru |
New-Partition -AssignDriveLetter -UseMaximumSize |
Format-Volume -FileSystem ReFS -Confirm:$false -Force
# Configures a dev drive for Windows CI to speed up Rust builds.
#
# The main performance win is disabling Windows Defender antivirus scanning
# on the build drive — AV scanning every .o, .rlib, .rmeta, and .exe during
# a Rust build is extremely expensive.
#
# Three code paths:
# 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):"
$Tmp = "$($Drive)/uv-tmp"
$Drive = "$($Volume.DriveLetter):"
# Create the directory ahead of time in an attempt to avoid race-conditions
New-Item $Tmp -ItemType Directory
# Mark as trusted and disable antivirus filtering
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 `
"DEV_DRIVE=$($Drive)" `
"TMP=$($Tmp)" `
"TEMP=$($Tmp)" `
"RUSTUP_HOME=$($Drive)/.rustup" `
"CARGO_HOME=$($Drive)/.cargo" `
"UV_WORKSPACE=$($Drive)/uv" `
"PATH=$($Drive)/.cargo/bin;$env:PATH" `
>> $env:GITHUB_ENV
"DEV_DRIVE=$($Drive)" `
"TMP=$($Tmp)" `
"TEMP=$($Tmp)" `
"RUSTUP_HOME=$($Drive)/.rustup" `
"CARGO_HOME=$($Drive)/.cargo" `
"DIOXUS_WORKSPACE=$($Drive)/dioxus" `
"PATH=$($Drive)/.cargo/bin;$env:PATH" `
>> $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-types",
"packages/cli",
"packages/cli-opt",
"packages/cli-config",
"packages/cli-telemetry",
"packages/core-macro",
@@ -22,7 +21,6 @@ members = [
"packages/interpreter",
"packages/liveview",
"packages/autofmt",
"packages/check",
"packages/devtools-types",
"packages/devtools",
"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-liveview = { path = "packages/liveview", 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-hotreload = { path = "packages/rsx-hotreload", 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" }
# 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-config = { path = "packages/cli-config", version = "0.7.3" }
@@ -229,7 +225,7 @@ futures = "0.3.31"
futures-channel = "0.3.31"
futures-util = { version = "0.3", default-features = false }
rustc-hash = "2.1.1"
wasm-bindgen = "0.2.105"
wasm-bindgen = "0.2.114"
wasm-bindgen-futures = "0.4.50"
js-sys = "0.3"
web-sys = { version = "0.3.77", default-features = false }
@@ -238,9 +234,12 @@ thiserror = "2.0.12"
prettyplease = { version = "0.2.35", features = ["verbatim"] }
const_format = "0.2.34"
cargo_toml = { version = "0.22.1" }
tauri-utils = { version = "=2.5.0" }
tauri-macos-sign = { version = "=2.2.0" }
tauri-bundler = { version = "=2.5.0" }
icns = "0.4.0"
image = "0.25"
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"
async-trait = "0.1.88"
axum = { version = "0.8.4", default-features = false }
@@ -282,8 +281,8 @@ ciborium = "0.2.2"
base64 = "0.22.1"
uuid = "1.17.0"
convert_case = "0.8.0"
tungstenite = { version = "0.27.0" }
tokio-tungstenite = { version = "0.27.0" }
tungstenite = { version = "0.28.0" }
tokio-tungstenite = { version = "0.28.0" }
gloo-timers = "0.3.0"
internment = { version = "0.8.6" }
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-html = "0.2.2"
path-absolutize = "3.1"
crossterm = { version = "0.29.0" }
crossterm = { version = "0.28.1" }
ratatui = { version = "0.29.0" }
shell-words = "1.1.0"
@@ -367,6 +366,11 @@ shell-words = "1.1.0"
keyboard-types = { version = "0.7", default-features = false }
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
# when we ship our CI builds, we turn on LTO which improves perf leftover by turning on incremental
[profile.release]
+2
View File
@@ -10,6 +10,8 @@ seeked = "seeked"
udid = "udid"
# Part of Blitz's API
unparented = "unparented"
# bundle type / archive
flate2="flate2"
[files]
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"] }
serde = { workspace = true, features = ["derive"] }
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 }
[features]
Generated
+16
View File
@@ -1,5 +1,20 @@
{
"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": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
@@ -67,6 +82,7 @@
},
"root": {
"inputs": {
"crane": "crane",
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay",
+42 -39
View File
@@ -5,8 +5,7 @@
systems.url = "github:nix-systems/default";
rust-overlay.url = "github:oxalica/rust-overlay";
# crane.url = "github:ipetkov/crane";
# crane.inputs.nixpkgs.follows = "nixpkgs";
crane.url = "github:ipetkov/crane";
};
outputs =
@@ -16,14 +15,18 @@
perSystem =
{
config,
self',
pkgs,
lib,
system,
...
}:
let
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [
inputs.rust-overlay.overlays.default
];
};
lib = pkgs.lib;
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [
"rust-src"
@@ -31,6 +34,7 @@
"clippy"
];
};
craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain;
rustBuildInputs = [
pkgs.openssl
pkgs.libiconv
@@ -49,6 +53,26 @@
];
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 =
package:
@@ -56,53 +80,32 @@
binary ? package,
features ? [ ],
}:
(pkgs.makeRustPlatform {
cargo = rustToolchain;
rustc = rustToolchain;
}).buildRustPackage
{
craneLib.buildPackage (
commonArgs
// {
pname = package;
version = cargoToml.package.version;
src = pkgs.lib.cleanSource ./.;
cargoLock.lockFile = ./Cargo.lock;
buildInputs = rustBuildInputs;
nativeBuildInputs = [
rustToolchain
pkgs.pkg-config
];
buildPhase = ''
mkdir -p .cargo
cp ${./Cargo.lock} Cargo.lock
cargo build --release --package ${package} ${
lib.concatStringsSep " " (map (f: "--features ${f}") features)
}
'';
installPhase = ''
inherit cargoArtifacts;
cargoExtraArgs = "--locked --package ${package} ${
lib.concatStringsSep " " (map (f: "--features ${f}") features)
}";
doCheck = false; # Disable tests to avoid building deps for them
installPhaseCommand = ''
mkdir -p $out/bin
ls -alR target/release
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
{
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
overlays = [
inputs.rust-overlay.overlays.default
];
};
packages.dioxus-cli = (
rustPackage "dioxus-cli" {
binary = "dx";
features = [ "no-downloads" ];
}
);
packages.default = self'.packages.dioxus-cli;
checks.dioxus-cli = self'.packages.dioxus-cli;
devShells.default = pkgs.mkShell {
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:
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:
+16 -17
View File
@@ -145,15 +145,15 @@ pub fn serve_asset(path: &str) -> Result<Response<Vec<u8>>, AssetServeError> {
/// - [x] iOS
/// - [x] Windows
/// - [x] Linux (appimage)
/// - [ ] Linux (rpm)
/// - [x] Linux (rpm)
/// - [x] Linux (deb)
/// - [ ] Android
/// - [x] Android (returns current exe.)
#[allow(unreachable_code)]
fn get_asset_root() -> PathBuf {
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
.parent()
.unwrap()
@@ -162,20 +162,19 @@ fn get_asset_root() -> PathBuf {
.join("Resources");
}
#[cfg(target_os = "linux")]
{
// In linux bundles, the assets are placed in the lib/$product_name directory
// bin/
// main
// lib/
// $product_name/
// assets/
// In linux bundles, the assets are placed in the lib/$product_name directory
// bin/
// main
// lib/
// $product_name/
// assets/
if cfg!(target_os = "linux") {
if let Some(product_name) = dioxus_cli_config::product_name() {
let lib_asset_path = || {
let path = cur_exe.parent()?.parent()?.join("lib").join(product_name);
path.exists().then_some(path)
};
if let Some(asset_dir) = lib_asset_path() {
let path = cur_exe
.parent()
.and_then(|parent| parent.parent())
.map(|root| root.join("lib").join(product_name));
if let Some(asset_dir) = path.filter(|path| path.exists()) {
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/"
license = "MIT OR Apache-2.0"
keywords = ["mobile", "gui", "cli", "dioxus", "wasm"]
rust-version = "1.81.0"
rust-version = "1.82.0"
[dependencies]
dioxus-autofmt = { workspace = true }
dioxus-check = { workspace = true }
dioxus-rsx-rosetta = { workspace = true }
dioxus-rsx = { workspace = true }
dioxus-rsx-hotreload = { workspace = true }
@@ -20,8 +19,6 @@ dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-core-types = { workspace = true }
dioxus-devtools-types = { workspace = true }
dioxus-cli-config = { workspace = true }
dioxus-cli-opt = { workspace = true }
dioxus-fullstack = { workspace = true }
dioxus-dx-wire-format = { workspace = true }
wasm-split-cli = { workspace = true }
depinfo = { workspace = true }
@@ -33,10 +30,8 @@ bytes = { workspace = true }
clap = { workspace = true, features = ["derive", "cargo"] }
convert_case = { workspace = true }
thiserror = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
# swc uses serde internals that break with newer versions of serde. We should move off of swc eventually,
# but pinning serde fixes the issue in the meantime
serde = { version = "=1.0.221", features = ["derive"] }
uuid = { workspace = true, features = ["v4", "v5", "v7"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
toml = { workspace = true }
cargo_toml = { workspace = true, features = ["features"] }
@@ -61,9 +56,10 @@ console = "0.16.0"
ctrlc = "3.4.7"
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"] }
tower-http = { workspace = true, features = ["full"] }
owo-colors = { workspace = true, features = ["supports-colors"] }
proc-macro2 = { workspace = true, features = ["span-locations"] }
syn = { workspace = true, features = [
"full",
@@ -72,13 +68,12 @@ syn = { workspace = true, features = [
"visit-mut",
] }
headers = "0.4.1"
walkdir = "2"
dunce = { workspace = true }
# tools download
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 }
git2 = "0.20.2"
@@ -95,9 +90,14 @@ toml_edit = "0.22.27"
prettyplease = { workspace = true }
# 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"
ignore = "0.4.23"
env_logger = { workspace = true }
const-serialize = { workspace = true, 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-07 = { workspace = true }
target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] }
wasm-encoder = "0.235.0"
# Extracting data from an executable
object = { workspace = true, features = ["all"] }
@@ -136,26 +135,26 @@ itertools = { workspace = true }
throbber-widgets-tui = "0.8.0"
unicode-segmentation = "1.12.0"
handlebars = "6.3.2"
strum = { version = "0.27.1", features = ["derive"] }
memmap = "0.7.0"
walrus = { workspace = true, features = ["parallel"] }
wasmparser = { workspace = true }
tauri-utils = { workspace = true }
tauri-macos-sign = { workspace = true }
tauri-bundler = { workspace = true }
# bundler dependencies
icns = { 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"
flate2 = "1.1.2"
tar = "0.4.44"
local-ip-address = "0.6.5"
dircpy = "0.3.19"
plist = "1.7.4"
memoize = "0.5.1"
backtrace = "0.3.74"
ar = "0.9.0"
wasm-bindgen-externref-xform = "0.2.100"
pdb = "0.8.0"
self_update = { version = "0.42.0", features = [
self_update = { version = "0.42.0", default-features = false, features = [
"archive-tar",
"archive-zip",
"compression-flate2",
@@ -163,7 +162,6 @@ self_update = { version = "0.42.0", features = [
] }
self-replace = "1.5.0"
cargo-config2 = { workspace = true }
posthog-rs = "0.3.5"
serde_json5 = "0.2.1"
fs2 = "0.4.3"
sentry-backtrace = "0.42.0"
@@ -175,9 +173,6 @@ home = "=0.5.11"
[target.'cfg(target_env = "musl")'.dependencies]
git2 = { version = "0.20.2", features = ["vendored-libgit2"] }
[build-dependencies]
built = { version = "0.8.0", features = ["git2"] }
[features]
default = []
tokio-console = ["dep:console-subscriber", "tokio/tracing"]
@@ -190,6 +185,8 @@ name = "dx"
[dev-dependencies]
escargot = "0.5"
indoc = "2.0.6"
pretty_assertions = { workspace = true }
[package.metadata.binstall]
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() {
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": {
"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": [
"string",
"null"
]
},
"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": [
"string",
"null"
+2 -2
View File
@@ -33,10 +33,10 @@ use std::{
path::{Path, PathBuf},
};
use crate::opt::AssetManifest;
use crate::Result;
use anyhow::{bail, Context};
use const_serialize::{deserialize_const, serialize_const, ConstVec};
use dioxus_cli_opt::AssetManifest;
use manganis::{AssetOptions, AssetVariant, BundledAsset, ImageFormat, ImageSize};
use manganis_core::{AndroidArtifactMetadata, SwiftPackageMetadata, SymbolData};
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
assets
.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)
for entry in write_entries {
+10 -5
View File
@@ -1,10 +1,10 @@
use crate::opt::process_file_to;
use crate::{
build::cache::ObjectCache, serve::WebServer, verbosity_or_default, BuildArtifacts,
BuildRequest, BuildStage, BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result,
RustcArgs, StructuredOutput,
};
use anyhow::{bail, Context, Error};
use dioxus_cli_opt::process_file_to;
use futures_util::{future::OptionFuture, pin_mut, FutureExt};
use itertools::Itertools;
use std::{
@@ -475,9 +475,12 @@ impl AppBuilder {
current,
total,
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::CopyingAssets {
@@ -509,7 +512,7 @@ impl AppBuilder {
}
BuilderUpdate::BuildFailed { err } => {
// 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 {
tracing::info!(json = %StructuredOutput::RustcOutput { message: message.clone() }, %message);
}
@@ -760,7 +763,8 @@ impl AppBuilder {
let to = asset_dir.join(bundled.bundled_path());
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}");
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
// hotreloading, we need to use the old asset location it was originally written to.
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());
if let Err(e) = res {
tracing::debug!("Failed to hotreload asset {e}");
+8 -1
View File
@@ -137,12 +137,19 @@ impl BuildContext {
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 {
stage: BuildStage::Compiling {
current: count,
total,
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)?;
// Create symbolic links (required for macOS framework structure)
let versions_dir = framework_dir.join("Versions");
#[cfg(unix)]
{
let versions_dir = framework_dir.join("Versions");
std::os::unix::fs::symlink("A", versions_dir.join("Current"))?;
std::os::unix::fs::symlink(
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
//! build time, target platform, etc.
use dioxus_cli_opt::AssetManifest;
use crate::opt::AssetManifest;
use serde::{Deserialize, Serialize};
#[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("wasm_bindgen8describe6inform")
|| name.contains("wasm_bindgen..describe..WasmDescribe")
|| name.contains("wasm_bindgen..closure..WasmClosure$GT$8describe")
|| name.contains("wasm_bindgen7closure16Closure$LT$T$GT$4wrap8describe")
|| (name.contains("wasm_bindgen..closure..WasmClosure") && name.contains("describe"))
|| (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
+32 -4
View File
@@ -320,6 +320,7 @@
//! - xbuild: <https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs>
use super::HotpatchModuleCache;
use crate::opt::{process_file_to, AssetManifest};
use crate::{
AndroidTools, AppManifest, BuildContext, BuildId, BundleFormat, DioxusConfig, Error,
LinkAction, LinkerFlavor, ObjectCache, Platform, Renderer, Result, RustcArgs, TargetArgs,
@@ -331,7 +332,6 @@ use cargo_toml::{Profile, Profiles, StripSetting};
use depinfo::RustcDepInfo;
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_opt::{process_file_to, AssetManifest};
use itertools::Itertools;
use krates::{cm::TargetKind, NodeId};
use manganis::{AssetOptions, BundledAsset, SwiftPackageMetadata};
@@ -1306,7 +1306,12 @@ impl BuildRequest {
}
Message::CompilerArtifact(artifact) => {
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);
}
// 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
let progress = ctx.tx.clone();
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
tokio::task::spawn_blocking(move || {
@@ -2292,7 +2298,7 @@ impl BuildRequest {
"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() {
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")
}
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:
///
/// - app dir (.app, .exe, .apk, etc)
@@ -5683,6 +5708,9 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
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 {
BundleFormat::Web => self.verify_web_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 name.starts_with(&prefix) && name.ends_with(".rlib") {
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));
}
}
-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.
fn merge_profiles(target: &mut Profiles, new: Profiles) {
if let Some(new_release) = new.release {
if target.release.is_none() {
target.release = Some(new_release);
if let Some(profile) = target.release.as_mut() {
merge_profile(profile, new_release);
} else {
merge_profile(target.release.as_mut().unwrap(), new_release);
target.release = Some(new_release);
}
}
if let Some(new_dev) = new.dev {
if target.dev.is_none() {
target.dev = Some(new_dev);
if let Some(profile) = target.dev.as_mut() {
merge_profile(profile, new_dev);
} else {
merge_profile(target.dev.as_mut().unwrap(), new_dev);
target.dev = Some(new_dev);
}
}
if let Some(new_test) = new.test {
if target.test.is_none() {
target.test = Some(new_test);
if let Some(profile) = target.test.as_mut() {
merge_profile(profile, new_test);
} else {
merge_profile(target.test.as_mut().unwrap(), new_test);
target.test = Some(new_test);
}
}
if let Some(new_bench) = new.bench {
if target.bench.is_none() {
target.bench = Some(new_bench);
if let Some(profile) = target.bench.as_mut() {
merge_profile(profile, new_bench);
} else {
merge_profile(target.bench.as_mut().unwrap(), new_bench);
target.bench = Some(new_bench);
}
}
#[allow(deprecated)]
if let Some(new_doc) = new.doc {
if target.doc.is_none() {
target.doc = Some(new_doc);
if let Some(profile) = target.doc.as_mut() {
merge_profile(profile, new_doc);
} else {
merge_profile(target.doc.as_mut().unwrap(), new_doc);
target.doc = Some(new_doc);
}
}
@@ -7,7 +7,7 @@ use std::{
path::{Path, PathBuf},
};
use crate::metadata::{
use super::metadata::{
AnyLoopInfo, AsyncInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, MatchInfo,
WhileInfo,
};
@@ -229,7 +229,7 @@ impl std::fmt::Display for Issue {
#[cfg(test)]
mod tests {
use crate::check_file;
use crate::check::check_file;
use indoc::indoc;
use pretty_assertions::assert_eq;
@@ -168,6 +168,7 @@ pub struct Span {
}
impl Span {
#[cfg(test)]
pub fn new_from_str(source_text: &str, start: LineColumn) -> Self {
let mut lines = source_text.lines();
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 crate::{
use {
issues::{Issue, IssueReport},
metadata::{
AnyLoopInfo, AsyncInfo, ClosureInfo, ComponentInfo, ConditionalInfo, FnInfo, ForInfo,
@@ -289,7 +291,7 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
#[cfg(test)]
mod tests {
use crate::metadata::{
use crate::check::metadata::{
AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, LineColumn, LoopInfo,
MatchInfo, Span, WhileInfo,
};
+2 -2
View File
@@ -1,8 +1,8 @@
use std::{fs::create_dir_all, path::PathBuf};
use crate::opt::process_file_to;
use crate::{extract_assets_from_file, Result, StructuredOutput};
use clap::Parser;
use dioxus_cli_opt::process_file_to;
use tracing::debug;
#[derive(Clone, Debug, Parser)]
@@ -28,7 +28,7 @@ impl BuildAssets {
destination_path.display(),
asset
);
process_file_to(asset.options(), &source_path, &destination_path)?;
process_file_to(asset.options(), &source_path, &destination_path, None)?;
}
Ok(StructuredOutput::Success)
+177 -162
View File
@@ -1,10 +1,10 @@
use crate::{AppBuilder, BuildArgs, BuildId, BuildMode, BuildRequest, BundleFormat};
use anyhow::{bail, Context};
use crate::{
AppBuilder, BuildArgs, BuildId, BuildMode, BuildRequest, BundleFormat, PackageType, Platform,
};
use anyhow::Context;
use path_absolutize::Absolutize;
use std::collections::HashMap;
use tauri_bundler::{BundleBinary, BundleSettings, PackageSettings, SettingsBuilder};
use walkdir::WalkDir;
use std::path::PathBuf;
use target_lexicon::Triple;
use super::*;
@@ -15,7 +15,7 @@ use super::*;
pub struct Bundle {
/// The package types to bundle
#[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.
///
@@ -37,15 +37,14 @@ impl Bundle {
pub(crate) async fn bundle(mut self) -> Result<StructuredOutput> {
tracing::info!("Bundling project...");
self.force_ios_bundle_device_target_if_needed()?;
let BuildTargets { client, server } = self.args.into_targets().await?;
let mut server_artifacts = None;
let client_artifacts =
AppBuilder::started(&client, BuildMode::Base { run: false }, BuildId::PRIMARY)?
.finish_build()
.await?;
tracing::info!(path = ?client.root_dir(), "Client build completed successfully! 🚀");
let mut server_artifacts = None;
if let Some(server) = server.as_ref() {
// 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! 🚀");
}
// If we're building for iOS, we need to bundle the iOS bundle
if client.bundle == BundleFormat::Ios && self.package_types.is_none() {
self.package_types = Some(vec![crate::PackageType::IosBundle]);
// Fill platform-specific defaults for package types when omitted.
if self.package_types.is_none() {
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![];
// Copy the server over if it exists
@@ -70,31 +71,24 @@ impl Bundle {
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 {
// By default, mac/win/linux work with tauri bundle
BundleFormat::MacOS | BundleFormat::Linux | BundleFormat::Windows => {
tracing::info!("Running desktop bundler...");
for bundle in Self::bundle_desktop(&client, &self.package_types)? {
// Desktop and Android platforms use package-type dispatch in the bundler module.
BundleFormat::MacOS
| BundleFormat::Ios
| 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);
}
}
// Web/ios can just use their root_dir
// Web can use its root_dir directly.
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::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
@@ -148,147 +142,45 @@ impl Bundle {
})
}
fn bundle_desktop(
async fn bundle_with_package_types(
build: &BuildRequest,
package_types: &Option<Vec<crate::PackageType>>,
) -> Result<Vec<tauri_bundler::Bundle>, Error> {
let krate = &build;
let exe = build.main_exe();
package_types: &Option<Vec<PackageType>>,
) -> Result<Vec<crate::bundler::Bundle>, Error> {
use anyhow::bail;
_ = 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();
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")?;
_ = std::fs::remove_dir_all(krate.bundle_dir(build.bundle));
let binaries = vec![
// We use the name of the exe but it has to be in the same directory
BundleBinary::new(krate.executable_name().to_string(), true)
.set_src_path(Some(exe.display().to_string())),
];
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 mut bundle_settings: BundleSettings = krate.config.bundle.clone().into();
// Check if required fields are provided instead of failing silently.
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 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)?;
// Check if required fields are provided instead of failing silently.
if build.config.bundle.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");
}
}
#[allow(deprecated)]
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 ctx = crate::bundler::BundleContext::new(build, package_types).await?;
let windows_icon_override = krate.config.bundle.windows.as_ref().map(|w| &w.icon_path);
if windows_icon_override.is_none() {
let icon_path = bundle_settings
.icon
.as_ref()
.and_then(|icons| icons.first());
tracing::debug!("Bundling project for {:?}", ctx.package_types());
if let Some(icon_path) = icon_path {
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| {
let bundles = ctx.bundle_project().await.inspect_err(|err| {
tracing::error!("Failed to bundle project: {:#?}", err);
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)");
@@ -297,4 +189,127 @@ impl Bundle {
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 {
tokio::fs::read_to_string(&_path)
.await
.map(|contents| dioxus_check::check_file(_path, &contents))
.map(|contents| crate::check::check_file(_path, &contents))
})
.await;
+60 -1
View File
@@ -63,7 +63,7 @@ where
{
fn augment_args(cmd: clap::Command) -> clap::Command {
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!()
}
}
#[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
fn escape_windows(s: Cow<str>) -> Cow<str> {
use std::iter::repeat;
let mut needs_escape = s.is_empty();
for ch in s.chars() {
match ch {
@@ -202,15 +200,15 @@ impl Print {
match chars.next() {
Some('"') => {
es.extend(repeat('\\').take(nslashes * 2 + 1));
es.extend(std::iter::repeat_n('\\', nslashes * 2 + 1));
es.push('"');
}
Some(c) => {
es.extend(repeat('\\').take(nslashes));
es.extend(std::iter::repeat_n('\\', nslashes));
es.push(c);
}
None => {
es.extend(repeat('\\').take(nslashes * 2));
es.extend(std::iter::repeat_n('\\', nslashes * 2));
break;
}
}
+6 -3
View File
@@ -82,10 +82,13 @@ impl RunArgs {
current,
total,
krate,
fresh,
} => {
tracing::debug!(
"[{bundle_format}] ({current}/{total}) Compiling {krate} ",
)
if !fresh {
tracing::debug!(
"[{bundle_format}] ({current}/{total}) Compiling {krate} ",
)
}
}
BuildStage::RunningBindgen => {
tracing::info!("[{bundle_format}] Running WASM bindgen")
+33 -8
View File
@@ -121,7 +121,7 @@ pub(crate) struct WixSettings {
pub(crate) fips_compliant: bool,
/// 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 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_**,
/// 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.
/// You can use Tauri's CLI to generate and print this code for you by running `tauri inspect wix-upgrade-code`.
/// By default, this code is generated as a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace.
///
/// 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.
#[serde(default)]
#[schemars(with = "Option<String>")]
@@ -146,28 +145,39 @@ pub(crate) struct WixSettings {
pub(crate) struct MacOsSettings {
#[serde(default)]
pub(crate) bundle_version: Option<String>,
#[serde(default)]
pub(crate) frameworks: Option<Vec<String>>,
#[serde(default)]
pub(crate) minimum_system_version: Option<String>,
#[serde(default)]
pub(crate) license: Option<String>,
#[serde(default)]
pub(crate) exception_domain: Option<String>,
#[serde(default)]
pub(crate) signing_identity: Option<String>,
#[serde(default)]
pub(crate) provider_short_name: Option<String>,
#[serde(default)]
pub(crate) entitlements: Option<String>,
#[serde(default)]
pub(crate) info_plist_path: Option<PathBuf>,
#[serde(default)]
pub(crate) bundle_name: Option<String>,
/// 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).
#[serde(default)]
pub files: HashMap<PathBuf, PathBuf>,
/// 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.
@@ -290,15 +300,19 @@ pub struct CustomSignCommandSettings {
pub args: Vec<String>,
}
#[derive(Clone, Copy, Debug, clap::ValueEnum, Serialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum, Serialize)]
pub(crate) enum PackageType {
/// The macOS application bundle (.app).
#[clap(name = "macos")]
MacOsBundle,
/// The iOS app bundle.
/// The raw iOS application bundle (.app).
#[clap(name = "ios")]
IosBundle,
IosApp,
/// The distributable iOS application archive (.ipa).
#[clap(name = "ipa")]
Ipa,
/// The Windows bundle (.msi).
#[clap(name = "msi")]
@@ -327,6 +341,14 @@ pub(crate) enum PackageType {
/// The Updater bundle (a patch of an existing app)
#[clap(name = "updater")]
Updater,
/// Android application package (.apk).
#[clap(name = "apk")]
Apk,
/// Android App Bundle (.aab).
#[clap(name = "aab")]
Aab,
}
impl FromStr for PackageType {
@@ -335,7 +357,8 @@ impl FromStr for PackageType {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"macos" => Ok(PackageType::MacOsBundle),
"ios" => Ok(PackageType::IosBundle),
"ios" => Ok(PackageType::IosApp),
"ipa" => Ok(PackageType::Ipa),
"msi" => Ok(PackageType::WindowsMsi),
"nsis" => Ok(PackageType::Nsis),
"deb" => Ok(PackageType::Deb),
@@ -343,6 +366,8 @@ impl FromStr for PackageType {
"appimage" => Ok(PackageType::AppImage),
"dmg" => Ok(PackageType::Dmg),
"updater" => Ok(PackageType::Updater),
"apk" => Ok(PackageType::Apk),
"aab" => Ok(PackageType::Aab),
_ => 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.
include!(concat!(env!("OUT_DIR"), "/built.rs"));
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
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 serde::{Deserialize, Serialize};
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::{
collections::HashMap,
env,
fmt::{Debug, Display, Write as _},
sync::atomic::{AtomicBool, Ordering},
time::Instant,
time::{Duration, Instant},
};
use std::{future::Future, panic::AssertUnwindSafe};
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
/// can access things like the logs.
pub async fn main<F>(run_app: impl FnOnce(Commands, Self) -> F) -> StructuredOutput
where
F: Future<Output = Result<StructuredOutput>>,
{
pub async fn main(
run_app: impl FnOnce(Commands, Self) -> Pin<Box<dyn Future<Output = Result<StructuredOutput>>>>,
) -> StructuredOutput {
let args = Cli::parse();
let tui_active = Arc::new(AtomicBool::new(false));
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`
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(args.verbosity.verbose)
.with_target(false)
.fmt_fields(
format::debug_fn(move |writer, field, value| {
if field.name() == "json" && !args.verbosity.json_output {
@@ -521,7 +520,7 @@ impl TraceController {
error_handled,
} = 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
_ = ph_event.insert_prop("is_ci", CliSettings::is_ci());
@@ -649,7 +648,7 @@ impl TraceController {
reset_cursor();
// 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 {
TraceContent::Text(text) => text,
TraceContent::Cargo(msg) => msg.message.to_string(),
@@ -729,7 +728,7 @@ impl TraceController {
true => {
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())
.collect::<Vec<_>>();
@@ -740,8 +739,7 @@ impl TraceController {
false => {
let mut msgs = self.telemetry_rx.lock().await;
let msg_list =
std::iter::from_fn(|| msgs.try_next().ok().flatten()).collect::<Vec<_>>();
let msg_list = std::iter::from_fn(|| msgs.try_recv().ok()).collect::<Vec<_>>();
if !msg_list.is_empty() {
let dest = Workspace::dioxus_data_dir()
@@ -1336,10 +1334,43 @@ impl Default for PrettyUptime {
impl FormatTime for PrettyUptime {
fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
let e = self.epoch.elapsed();
write!(w, "{:4}.{:2}s", e.as_secs(), e.subsec_millis())
Self::write_elapsed(self.epoch.elapsed(), w)
}
}
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.
///
/// 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
/// across the CLI.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Reporter {
pub session_id: Uuid,
pub distinct_id: Uuid,
struct Reporter {
session_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)]
mod build;
mod bundle_utils;
mod bundler;
mod cargo_toml;
mod check;
mod cli;
mod config;
mod devcfg;
mod dx_build_info;
mod error;
mod esbuild;
mod fastfs;
mod logging;
mod opt;
mod platform;
mod rustcwrapper;
mod serve;
@@ -53,25 +56,27 @@ async fn main() -> ExitCode {
}
// Run under the tracing collector so we can capture errors/panics.
let result = TraceController::main(|args, tracer| async move {
match args {
Commands::Serve(opts) => opts.serve(&tracer).await,
Commands::Translate(opts) => opts.translate(),
Commands::New(opts) => opts.create().await,
Commands::Init(opts) => opts.init().await,
Commands::Config(opts) => opts.config().await,
Commands::Autoformat(opts) => opts.autoformat().await,
Commands::Check(opts) => opts.check().await,
Commands::Build(opts) => opts.build().await,
Commands::Bundle(opts) => opts.bundle().await,
Commands::Run(opts) => opts.run().await,
Commands::SelfUpdate(opts) => opts.self_update().await,
Commands::Tools(BuildTools::BuildAssets(opts)) => opts.run().await,
Commands::Tools(BuildTools::HotpatchTip(opts)) => opts.run().await,
Commands::Doctor(opts) => opts.doctor().await,
Commands::Print(opts) => opts.print().await,
Commands::Components(opts) => opts.run().await,
}
let result = TraceController::main(|args, tracer| {
Box::pin(async move {
match args {
Commands::Serve(opts) => opts.serve(&tracer).await,
Commands::Translate(opts) => opts.translate(),
Commands::New(opts) => opts.create().await,
Commands::Init(opts) => opts.init().await,
Commands::Config(opts) => opts.config().await,
Commands::Autoformat(opts) => opts.autoformat().await,
Commands::Check(opts) => opts.check().await,
Commands::Build(opts) => opts.build().await,
Commands::Bundle(opts) => opts.bundle().await,
Commands::Run(opts) => opts.run().await,
Commands::SelfUpdate(opts) => opts.self_update().await,
Commands::Tools(BuildTools::BuildAssets(opts)) => opts.run().await,
Commands::Tools(BuildTools::HotpatchTip(opts)) => opts.run().await,
Commands::Doctor(opts) => opts.doctor().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.
File renamed without changes.
@@ -3,7 +3,7 @@ use manganis::{AssetOptions, CssModuleAssetOptions, FolderAssetOptions};
use manganis_core::{AssetVariant, CssAssetOptions, ImageAssetOptions, JsAssetOptions};
use std::path::Path;
use crate::css::{process_css_module, process_scss};
use crate::opt::css::{process_css_module, process_scss};
use super::{
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
pub fn process_file_to(
pub(crate) fn process_file_to(
options: &AssetOptions,
source: &Path,
output_path: &Path,
esbuild_path: Option<&Path>,
) -> 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
@@ -25,6 +26,7 @@ pub(crate) fn process_file_to_with_options(
source: &Path,
output_path: &Path,
in_folder: bool,
esbuild_path: Option<&Path>,
) -> anyhow::Result<()> {
// 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
@@ -61,7 +63,7 @@ pub(crate) fn process_file_to_with_options(
process_scss(options, source, &temp_path)?;
}
ResolvedAssetType::Js(options) => {
process_js(options, source, &temp_path, !in_folder)?;
process_js(options, source, &temp_path, !in_folder, esbuild_path)?;
}
ResolvedAssetType::Image(options) => {
process_image(options, source, &temp_path)?;
@@ -70,7 +72,7 @@ pub(crate) fn process_file_to_with_options(
process_json(source, &temp_path)?;
}
ResolvedAssetType::Folder(_) => {
process_folder(source, &temp_path)?;
process_folder(source, &temp_path, esbuild_path)?;
}
ResolvedAssetType::File => {
let source_file = std::fs::File::open(source)?;
@@ -2,10 +2,14 @@ use std::path::Path;
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
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
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 output_path = output_folder.join(file.strip_prefix(source)?);
if metadata.is_dir() {
process_folder(&file, &output_path)
process_folder(&file, &output_path, esbuild_path)
} 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)
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(
&manganis_core::AssetOptions::builder().into_asset_options(),
input_path,
output_path,
true,
esbuild_path,
)?;
Ok(())
}
@@ -6,7 +6,7 @@ use std::{
path::{Path, PathBuf},
};
use crate::{
use crate::opt::{
css::hash_scss,
file::{resolve_asset_options, ResolvedAssetType},
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();
options.hash(&mut hash);
// Hash the version of CLI opt
hash.write(crate::build_info::version().as_bytes());
// Hash the version of the CLI
hash.write(crate::dx_build_info::PKG_VERSION.as_bytes());
hash_file_with_options(options, source, &mut hash, false)?;
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
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();
match AssetHash::hash_file_contents(asset.options(), source) {
Ok(hash) => {
@@ -1,12 +1,8 @@
use std::path::Path;
use anyhow::Context;
use jpg::compress_jpg;
use image::{DynamicImage, EncodableLayout};
use manganis_core::{ImageAssetOptions, ImageFormat, ImageSize};
use png::compress_png;
mod jpg;
mod png;
use std::io::{BufWriter, Write};
use std::path::Path;
pub(crate) fn process_image(
image_options: &ImageAssetOptions,
@@ -71,3 +67,70 @@ pub(crate) fn process_image(
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 rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
mod build_info;
mod css;
mod file;
mod folder;
@@ -16,14 +11,14 @@ mod image;
mod js;
mod json;
pub use file::process_file_to;
pub use hash::add_hash_to_asset;
pub(crate) use file::process_file_to;
pub(crate) use hash::add_hash_to_asset;
/// A manifest of all assets collected from dependencies
///
/// This will be filled in primarily by incremental compilation artifacts.
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
pub struct AssetManifest {
pub(crate) struct AssetManifest {
/// Map of bundled asset name to the asset itself
assets: BTreeMap<PathBuf, HashSet<BundledAsset>>,
}
@@ -88,41 +83,4 @@ impl AssetManifest {
.flat_map(|assets| assets.iter())
.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
pub fn render(&mut self, runner: &AppServer, server: &WebServer) {
if !self.interactive {
self.emit_logs_to_tracing();
return;
}
@@ -1094,11 +1095,31 @@ impl Output {
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 {
fn drop(&mut self) {
if !self.interactive {
self.emit_logs_to_tracing();
return;
}
+2 -2
View File
@@ -300,7 +300,7 @@ impl AppServer {
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
while let Some(event) = self.watcher_rx.try_next().ok().flatten() {
while let Ok(event) = self.watcher_rx.try_recv() {
changes.push(event);
}
@@ -1313,7 +1313,7 @@ impl AppServer {
let depth = relative.components().count();
let is_better = best_match
.as_ref()
.map_or(true, |(_, best_depth)| depth < *best_depth);
.is_none_or(|(_, best_depth)| depth < *best_depth);
if is_better {
best_match = Some((krate.name.replace('-', "_"), depth));
}
+1 -1
View File
@@ -26,7 +26,7 @@ use futures_util::{
StreamExt,
};
use hyper::HeaderMap;
use rustls::crypto::{aws_lc_rs::default_provider, CryptoProvider};
use rustls::crypto::{ring::default_provider, CryptoProvider};
use serde::{Deserialize, Serialize};
use std::{
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
// hardcoded for now to get around github api rate limits
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")) {
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")) {
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")) {
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")) {
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";
+1 -1
View File
@@ -479,7 +479,7 @@ impl VirtualDom {
/// Queue any pending events
fn queue_events(&mut self) {
// 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 {
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)),
+9 -2
View File
@@ -131,9 +131,16 @@ impl Config {
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 {
self.data_dir = Some(path.into());
self
+1
View File
@@ -231,6 +231,7 @@ impl EditWebsocket {
let hex_encoded_server_key = encode_key_string(&current_server_location.server_key);
let mut location = None;
#[allow(clippy::result_large_err)]
let on_request = |req: &Request, res| {
// Try to parse the webview id and key from the path
let path = req.uri().path();
+1
View File
@@ -3,6 +3,7 @@
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(unexpected_cfgs)]
mod android_sync_lock;
mod app;
+13 -2
View File
@@ -260,12 +260,23 @@ impl WebviewInstance {
unsafe {
let window: id = window.ns_window() as id;
#[allow(unexpected_cfgs)]
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 asset_handlers = AssetHandlerRegistry::new();
let edits = WebviewEdits::new(dom.runtime(), edit_queue.clone());
+1
View File
@@ -94,6 +94,7 @@ pub enum BuildStage {
current: usize,
total: usize,
krate: String,
fresh: bool,
},
RunningBindgen,
SplittingBundle,
+84 -223
View File
@@ -1,251 +1,112 @@
// @ts-check
const { defineConfig, devices } = require("@playwright/test");
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
// Copy a directory to a temp location for tests that modify source files (hot-patch tests).
// Done in JS so the webServer command is a simple `dx serve` with a `cwd`, which keeps
// stdout/stderr piping clean on both Unix and Windows cmd.exe.
function copyToTemp(src, dest) {
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 = [];
let grep = undefined;
let grepInvert = undefined;
const repoRoot = path.resolve(__dirname, "..", "..");
const dx = path.join(repoRoot, "target", "release", process.platform === "win32" ? "dx.exe" : "dx");
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") {
webServer = [
{
cwd: path.join(process.cwd(), "windows-headless"),
command:
"cargo run --package dioxus-cli --release -- run --force-sequential",
port: 8787,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
];
grep = /windows/;
} else {
webServer = [
{
command:
"cargo run --package dioxus-playwright-liveview-test --bin dioxus-playwright-liveview-test",
port: 3030,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "web"),
command:
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 9990',
port: 9990,
timeout: 50 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
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",
},
];
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` });
}
// Determine which servers to start based on spec files in argv and platform.
// On Windows only windows.spec.js runs (grep filter below), so don't bother
// starting servers for tests that will never execute — they just crash and
// pollute the logs.
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
? ALL_SERVERS.filter((s) => s.specs.some((spec) => specArgs.includes(spec)))
: isWindows
? ALL_SERVERS.filter((s) => s.specs.some((spec) => spec.includes("windows")))
: ALL_SERVERS.filter((s) => !s.specs.some((spec) => spec.includes("windows")));
// Run any setup functions (e.g. copying source to temp dirs for hot-patch tests)
// Only happens on initialize
//
// Build dx once: main process builds and sets env var; workers inherit it and skip.
// todo: implement proper fixtures https://www.youtube.com/watch?v=3i6cJUFO_m4
if (!process.env._DX_BUILT) {
for (const s of activeServers) {
if (s.setup) s.setup();
}
execSync("cargo build --package dioxus-cli --release", {
cwd: repoRoot,
env: { ...process.env, CARGO_TERM_PROGRESS_WHEN: "never" },
stdio: "inherit",
});
grepInvert = /windows/;
process.env._DX_BUILT = "1";
}
/**
* @see https://playwright.dev/docs/test-configuration
*/
module.exports = defineConfig({
testDir: ".",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
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",
// Increase the timeout for navigations to give dx time to build the project
navigationTimeout: 50 * 60 * 1000,
},
timeout: 50 * 60 * 1000,
/* Configure projects for major browsers */
webServer: activeServers.map((s) => ({
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: [
{
name: "chromium",
grep,
grepInvert,
grep: process.platform === "win32" ? /windows/ : undefined,
grepInvert: process.platform !== "win32" ? /windows/ : undefined,
use: { ...devices["Desktop Chrome"] },
},
],
/* Run your local dev server before starting the tests */
webServer,
});
@@ -8,7 +8,7 @@ publish = false
[dependencies]
dioxus = { path = "../../dioxus", features = ["web"] }
wasm-bindgen = { version = "0.2.100" }
wasm-bindgen = { version = "0.2.114" }
web-sys = { version = "*", features = ["MouseEvent"] }
[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 {
while rx.next().await.is_some() {
// Remove any pending updates
while rx.try_next().is_ok() {}
while rx.try_recv().is_ok() {}
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
.visibility
.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(
input.trait_.unwrap().1,
input_trait.1.clone(),
"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)?;
// 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
let (_, trait_generics, _) = input.generics.split_for_impl();
input.trait_ = Some((