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:
1 parent
be7296f4f0
commit
b2d01e6779
83 files changed
+7704
-5351
No files matched your search
+376
-170
@@ -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,7 +507,7 @@ jobs:
|
||||
}
|
||||
- {
|
||||
target: aarch64-linux-android,
|
||||
os: blacksmith-4vcpu-ubuntu-2404,
|
||||
os: warp-ubuntu-latest-x64-4x,
|
||||
toolchain: "1.88.0",
|
||||
cross: true,
|
||||
command: "build",
|
||||
@@ -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
|
||||
'
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,19 +1,67 @@
|
||||
# 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 |
|
||||
# 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
|
||||
|
||||
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 -FileSystem ReFS -Confirm:$false -Force
|
||||
|
||||
Write-Output $Volume
|
||||
Format-Volume -DevDrive -Confirm:$false -Force
|
||||
|
||||
$Drive = "$($Volume.DriveLetter):"
|
||||
$Tmp = "$($Drive)/uv-tmp"
|
||||
|
||||
# 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)" `
|
||||
@@ -21,6 +69,6 @@ Write-Output `
|
||||
"TEMP=$($Tmp)" `
|
||||
"RUSTUP_HOME=$($Drive)/.rustup" `
|
||||
"CARGO_HOME=$($Drive)/.cargo" `
|
||||
"UV_WORKSPACE=$($Drive)/uv" `
|
||||
"DIOXUS_WORKSPACE=$($Drive)/dioxus" `
|
||||
"PATH=$($Drive)/.cargo/bin;$env:PATH" `
|
||||
>> $env:GITHUB_ENV
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+15
-11
@@ -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]
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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} ${
|
||||
inherit cargoArtifacts;
|
||||
cargoExtraArgs = "--locked --package ${package} ${
|
||||
lib.concatStringsSep " " (map (f: "--features ${f}") features)
|
||||
}
|
||||
'';
|
||||
installPhase = ''
|
||||
}";
|
||||
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";
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -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.
|
||||
@@ -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};
|
||||
@@ -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"] }
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
built::write_built_file().expect("Failed to acquire build-time information");
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,10 +475,13 @@ impl AppBuilder {
|
||||
current,
|
||||
total,
|
||||
krate,
|
||||
fresh,
|
||||
..
|
||||
} => {
|
||||
if !fresh {
|
||||
tracing::info!("Compiled [{current:>3}/{total}]: {krate}");
|
||||
}
|
||||
}
|
||||
BuildStage::RunningBindgen => tracing::info!("Running wasm-bindgen..."),
|
||||
BuildStage::CopyingAssets {
|
||||
current,
|
||||
@@ -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}");
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
+165
-150
@@ -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> {
|
||||
package_types: &Option<Vec<PackageType>>,
|
||||
) -> Result<Vec<crate::bundler::Bundle>, Error> {
|
||||
use anyhow::bail;
|
||||
|
||||
if matches!(
|
||||
build.bundle,
|
||||
BundleFormat::MacOS | BundleFormat::Linux | BundleFormat::Windows
|
||||
) {
|
||||
let krate = &build;
|
||||
let exe = build.main_exe();
|
||||
|
||||
_ = std::fs::remove_dir_all(krate.bundle_dir(build.bundle));
|
||||
|
||||
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")?;
|
||||
|
||||
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 bundle_settings: BundleSettings = krate.config.bundle.clone().into();
|
||||
std::fs::copy(&exe, krate.bundle_dir(build.bundle).join(&name)).with_context(|| {
|
||||
"Failed to copy the output executable into the bundle directory"
|
||||
})?;
|
||||
|
||||
// Check if required fields are provided instead of failing silently.
|
||||
if bundle_settings.identifier.is_none() {
|
||||
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 bundle_settings.publisher.is_none() {
|
||||
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");
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
}
|
||||
}
|
||||
let ctx = crate::bundler::BundleContext::new(build, package_types).await?;
|
||||
|
||||
#[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);
|
||||
tracing::debug!("Bundling project for {:?}", ctx.package_types());
|
||||
|
||||
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());
|
||||
|
||||
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())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,11 +82,14 @@ impl RunArgs {
|
||||
current,
|
||||
total,
|
||||
krate,
|
||||
fresh,
|
||||
} => {
|
||||
if !fresh {
|
||||
tracing::debug!(
|
||||
"[{bundle_format}] ({current}/{total}) Compiling {krate} ",
|
||||
)
|
||||
}
|
||||
}
|
||||
BuildStage::RunningBindgen => {
|
||||
tracing::info!("[{bundle_format}] Running WASM bindgen")
|
||||
}
|
||||
|
||||
@@ -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")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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,7 +56,8 @@ async fn main() -> ExitCode {
|
||||
}
|
||||
|
||||
// Run under the tracing collector so we can capture errors/panics.
|
||||
let result = TraceController::main(|args, tracer| async move {
|
||||
let result = TraceController::main(|args, tracer| {
|
||||
Box::pin(async move {
|
||||
match args {
|
||||
Commands::Serve(opts) => opts.serve(&tracer).await,
|
||||
Commands::Translate(opts) => opts.translate(),
|
||||
@@ -72,6 +76,7 @@ async fn main() -> ExitCode {
|
||||
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(())
|
||||
}
|
||||
@@ -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(|_| ())
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -231,6 +231,7 @@ impl EditWebsocket {
|
||||
let hex_encoded_server_key = encode_key_string(¤t_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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -94,6 +94,7 @@ pub enum BuildStage {
|
||||
current: usize,
|
||||
total: usize,
|
||||
krate: String,
|
||||
fresh: bool,
|
||||
},
|
||||
RunningBindgen,
|
||||
SplittingBundle,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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((
|
||||
|
||||
Reference in new issue
Block a user