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
+7756
-5403
No files matched your search
+377
-171
@@ -1,34 +1,19 @@
|
|||||||
# Whenever an open PR is updated, the workflow will be triggered
|
# Whenever an open PR is updated, the workflow will be triggered
|
||||||
#
|
#
|
||||||
# This can get expensive, so we do a lot of caching and checks to prevent unnecessary runs
|
# This can get expensive, so we do a lot of caching and checks to prevent unnecessary runs
|
||||||
|
# Language linting (typos, link-check) runs on every push; Rust jobs are gated behind path checks
|
||||||
|
|
||||||
name: Rust CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
|
||||||
- packages/**
|
|
||||||
- examples/**
|
|
||||||
- docs/guide/**
|
|
||||||
- src/**
|
|
||||||
- .github/**
|
|
||||||
- lib.rs
|
|
||||||
- Cargo.toml
|
|
||||||
- Makefile.toml
|
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
|
||||||
- packages/**
|
|
||||||
- examples/**
|
|
||||||
- src/**
|
|
||||||
- .github/**
|
|
||||||
- lib.rs
|
|
||||||
- Cargo.toml
|
|
||||||
|
|
||||||
# workflow_dispatch:
|
# workflow_dispatch:
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -38,110 +23,113 @@ concurrency:
|
|||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
CARGO_INCREMENTAL: 0 # todo(jon): cargo-cache wipes incremental artifacts, but we eventually want to cache them
|
CARGO_INCREMENTAL: 0 # todo(jon): cargo-cache wipes incremental artifacts, but we eventually want to cache them
|
||||||
|
# CARGO_PROFILE_DEV_PACKAGE_STAR_STRIP: debuginfo # strip debug info from deps to shrink cache
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
rust_nightly: nightly-2025-10-05
|
rust_nightly: nightly-2025-10-05
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-msrv:
|
# ---------------------------------------------------------------------------
|
||||||
if: github.event.pull_request.draft == false
|
# Path gate — determines whether Rust jobs should run
|
||||||
name: Check MSRV
|
# ---------------------------------------------------------------------------
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
check-paths:
|
||||||
|
runs-on: warp-ubuntu-latest-x64-4x
|
||||||
|
outputs:
|
||||||
|
rust: ${{ steps.filter.outputs.rust }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: dtolnay/rust-toolchain@1.88.0
|
- uses: dorny/paths-filter@v3
|
||||||
- uses: Swatinem/rust-cache@v2
|
id: filter
|
||||||
with:
|
with:
|
||||||
cache-all-crates: "true"
|
filters: |
|
||||||
# https://github.com/foresterre/cargo-msrv/blob/4345edfe3f4fc91cc8ae6c7d6804c0748fae92ae/.github/workflows/msrv.yml
|
rust:
|
||||||
- name: install_cargo_msrv
|
- 'packages/**'
|
||||||
run: cargo install cargo-msrv --all-features --version 0.16.3 --locked
|
- 'examples/**'
|
||||||
- name: version_of_cargo_msrv
|
- 'docs/guide/**'
|
||||||
run: cargo msrv --version
|
- 'src/**'
|
||||||
- name: run_cargo_msrv
|
- '.github/**'
|
||||||
run: cargo msrv --output-format json verify -- cargo check
|
- 'lib.rs'
|
||||||
- name: run_cargo_msrv_on_verify_failure
|
- 'Cargo.toml'
|
||||||
if: ${{ failure() }}
|
- 'Makefile.toml'
|
||||||
run: cargo msrv --output-format json -- cargo check
|
|
||||||
|
|
||||||
test:
|
# ---------------------------------------------------------------------------
|
||||||
|
# Language linting — runs on every push
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
typos:
|
||||||
if: github.event.pull_request.draft == false
|
if: github.event.pull_request.draft == false
|
||||||
name: Test Suite
|
name: "Lint | Typos"
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: warp-ubuntu-latest-x64-4x
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- name: Check for typos
|
||||||
|
uses: crate-ci/typos@master
|
||||||
|
|
||||||
|
link-check:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
name: "Lint | Links"
|
||||||
|
runs-on: warp-ubuntu-latest-x64-4x
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- name: Restore lychee cache
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: .lycheecache
|
||||||
|
key: cache-lychee-${{ github.sha }}
|
||||||
|
restore-keys: cache-lychee-
|
||||||
|
- name: Run lychee
|
||||||
|
uses: lycheeverse/lychee-action@v2
|
||||||
|
with:
|
||||||
|
args: --config ./lychee.toml './**/*.md'
|
||||||
|
fail: true
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Rust jobs — only run when relevant paths change
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
lint:
|
||||||
|
needs: check-paths
|
||||||
|
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
|
||||||
|
name: "Lint | Clippy, fmt, schema, msrv"
|
||||||
|
runs-on: warp-ubuntu-latest-x64-4x
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- name: Free Disk Space
|
|
||||||
uses: ./.github/actions/free-disk-space
|
|
||||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||||
with:
|
with:
|
||||||
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
|
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
|
||||||
version: 1.0
|
version: 1.0
|
||||||
- uses: dtolnay/rust-toolchain@1.88.0
|
- uses: dtolnay/rust-toolchain@1.94.0
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
|
- uses: cargo-bins/cargo-binstall@main
|
||||||
|
- name: Install cargo-msrv
|
||||||
|
run: cargo binstall cargo-msrv -y --force
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
cache-all-crates: "true"
|
cache-all-crates: "true"
|
||||||
- uses: browser-actions/setup-firefox@latest
|
cache-workspace-crates: "true"
|
||||||
- run: cargo test --lib --bins --tests --examples --workspace --exclude dioxus-desktop
|
cache-provider: "warpbuild"
|
||||||
|
# save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
release-test:
|
- name: Rustfmt
|
||||||
if: github.event.pull_request.draft == false
|
run: cargo fmt --all -- --check
|
||||||
name: Test Suite with Optimizations
|
- name: Clippy
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
run: cargo clippy --workspace --examples --tests --all-features --all-targets -- -D warnings
|
||||||
steps:
|
- name: Check schema
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Free Disk Space
|
|
||||||
uses: ./.github/actions/free-disk-space
|
|
||||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
|
||||||
with:
|
|
||||||
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
|
|
||||||
version: 1.0
|
|
||||||
- uses: dtolnay/rust-toolchain@1.88.0
|
|
||||||
with:
|
|
||||||
components: rustfmt, clippy
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
cache-all-crates: "true"
|
|
||||||
- uses: browser-actions/setup-firefox@latest
|
|
||||||
- run: cargo test --lib --bins --tests --examples --workspace --exclude dioxus-desktop --profile release-unoptimized
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
name: Rustfmt
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- uses: dtolnay/rust-toolchain@1.88.0
|
|
||||||
with:
|
|
||||||
components: rustfmt
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
cache-all-crates: "true"
|
|
||||||
- run: cargo fmt --all -- --check
|
|
||||||
|
|
||||||
schema:
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
name: Check Schema
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- uses: dtolnay/rust-toolchain@1.88.0
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
cache-all-crates: "true"
|
|
||||||
- name: Generate schema
|
|
||||||
run: cargo run -p dioxus-cli -- config schema --out packages/cli/schema.json
|
run: cargo run -p dioxus-cli -- config schema --out packages/cli/schema.json
|
||||||
- name: Check for uncommitted changes
|
- name: Verify schema is up to date
|
||||||
run: |
|
run: |
|
||||||
if ! git diff --exit-code packages/cli/schema.json; then
|
if ! git diff --exit-code packages/cli/schema.json; then
|
||||||
echo "::error::Schema is out of date. Run 'dx config schema --out packages/cli/schema.json' and commit the changes."
|
echo "::error::Schema is out of date. Run 'dx config schema --out packages/cli/schema.json' and commit the changes."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
- name: Verify MSRV
|
||||||
|
run: cargo msrv --output-format json verify -- cargo check
|
||||||
|
- name: Find MSRV on verify failure
|
||||||
|
if: ${{ failure() }}
|
||||||
|
run: cargo msrv --output-format json -- cargo check
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
if: github.event.pull_request.draft == false
|
needs: check-paths
|
||||||
name: Docs
|
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
name: "Docs | Build"
|
||||||
|
runs-on: warp-ubuntu-latest-x64-4x
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||||
@@ -155,6 +143,9 @@ jobs:
|
|||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
cache-all-crates: "true"
|
cache-all-crates: "true"
|
||||||
|
cache-workspace-crates: "true"
|
||||||
|
cache-provider: "warpbuild"
|
||||||
|
# save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
- name: "doc --lib --all-features"
|
- name: "doc --lib --all-features"
|
||||||
run: |
|
run: |
|
||||||
cargo doc --workspace --no-deps --all-features --document-private-items
|
cargo doc --workspace --no-deps --all-features --document-private-items
|
||||||
@@ -162,9 +153,10 @@ jobs:
|
|||||||
RUSTDOCFLAGS: -Dwarnings --document-private-items
|
RUSTDOCFLAGS: -Dwarnings --document-private-items
|
||||||
|
|
||||||
test-docs:
|
test-docs:
|
||||||
if: github.event.pull_request.draft == false
|
needs: check-paths
|
||||||
name: Test Docs
|
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
name: "Docs | Test"
|
||||||
|
runs-on: warp-ubuntu-latest-x64-4x
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||||
@@ -178,83 +170,213 @@ jobs:
|
|||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
cache-all-crates: "true"
|
cache-all-crates: "true"
|
||||||
|
cache-workspace-crates: "true"
|
||||||
|
cache-provider: "warpbuild"
|
||||||
|
# save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
- name: "doc --lib --all-features"
|
- name: "doc --lib --all-features"
|
||||||
run: |
|
run: |
|
||||||
cargo test --doc --workspace --all-features
|
cargo test --doc --workspace --all-features
|
||||||
|
|
||||||
check:
|
bundle-linux-smoke:
|
||||||
if: github.event.pull_request.draft == false
|
needs: check-paths
|
||||||
name: Check
|
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
name: "Bundle | Linux"
|
||||||
|
runs-on: warp-ubuntu-latest-x64-4x
|
||||||
|
timeout-minutes: 45
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
|
- name: Free Disk Space
|
||||||
|
uses: ./.github/actions/free-disk-space
|
||||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||||
with:
|
with:
|
||||||
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
|
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev patchelf desktop-file-utils librsvg2-dev
|
||||||
version: 1.0
|
version: 1.0
|
||||||
- uses: dtolnay/rust-toolchain@1.88.0
|
- uses: dtolnay/rust-toolchain@1.88.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
cache-all-crates: "true"
|
cache-all-crates: "true"
|
||||||
- run: cargo check --workspace --all-features --all-targets
|
cache-workspace-crates: "true"
|
||||||
|
cache-provider: "warpbuild"
|
||||||
|
# save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
- name: Bundle hotdog as Linux packages
|
||||||
|
working-directory: ./examples/01-app-demos/hotdog
|
||||||
|
run: cargo run --manifest-path ../../../Cargo.toml -p dioxus-cli -- bundle --platform desktop --package-types appimage --package-types deb --package-types rpm
|
||||||
|
- name: Verify Linux bundle outputs
|
||||||
|
run: |
|
||||||
|
find target -name '*.AppImage' -print
|
||||||
|
find target -name '*.deb' -print
|
||||||
|
find target -name '*.rpm' -print
|
||||||
|
test -n "$(find target -name '*.AppImage' -print -quit)"
|
||||||
|
test -n "$(find target -name '*.deb' -print -quit)"
|
||||||
|
test -n "$(find target -name '*.rpm' -print -quit)"
|
||||||
|
- uses: actions/upload-artifact@v6
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: hotdog-linux-bundles
|
||||||
|
path: |
|
||||||
|
target/**/*.AppImage
|
||||||
|
target/**/*.deb
|
||||||
|
target/**/*.rpm
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
clippy:
|
bundle-macos-smoke:
|
||||||
if: github.event.pull_request.draft == false
|
needs: check-paths
|
||||||
name: Clippy
|
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
name: "Bundle | macOS"
|
||||||
|
runs-on: warp-macos-latest-arm64-6x
|
||||||
|
timeout-minutes: 45
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
- uses: dtolnay/rust-toolchain@1.88.0
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
|
cache-all-crates: "true"
|
||||||
version: 1.0
|
cache-workspace-crates: "true"
|
||||||
- uses: dtolnay/rust-toolchain@1.90.0
|
cache-provider: "warpbuild"
|
||||||
|
# save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
- name: Bundle hotdog as macOS packages
|
||||||
|
working-directory: ./examples/01-app-demos/hotdog
|
||||||
|
run: cargo run --manifest-path ../../../Cargo.toml -p dioxus-cli -- bundle --platform desktop --package-types macos --package-types dmg
|
||||||
|
- name: Verify macOS bundle outputs
|
||||||
|
run: |
|
||||||
|
find target -type d -name '*.app' -print
|
||||||
|
find target -type f -name '*.dmg' -print
|
||||||
|
test -n "$(find target -type d -name '*.app' -print -quit)"
|
||||||
|
test -n "$(find target -type f -name '*.dmg' -print -quit)"
|
||||||
|
- uses: actions/upload-artifact@v6
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: hotdog-macos-bundles
|
||||||
|
path: |
|
||||||
|
target/**/*.app
|
||||||
|
target/**/*.dmg
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
windows-bundle-smoke:
|
||||||
|
needs: check-paths
|
||||||
|
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
|
||||||
|
name: "Bundle | Windows"
|
||||||
|
runs-on: warp-windows-2025-x64-8x
|
||||||
|
timeout-minutes: 45
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- name: Setup Dev Drive
|
||||||
|
run: ${{ github.workspace }}/.github/workflows/setup-dev-drive.ps1
|
||||||
|
- name: Copy Repo to Dev Drive
|
||||||
|
run: Copy-Item -Path "${{ github.workspace }}" -Destination "$env:DIOXUS_WORKSPACE" -Recurse
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@1.88.0
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
|
key: "windows-bundle-${{ runner.os }}"
|
||||||
cache-all-crates: "true"
|
cache-all-crates: "true"
|
||||||
- run: cargo clippy --workspace --examples --tests --all-features --all-targets -- -D warnings
|
cache-workspace-crates: "true"
|
||||||
|
cache-provider: "warpbuild"
|
||||||
|
workspaces: ${{ env.DIOXUS_WORKSPACE }}
|
||||||
|
- name: Bundle hotdog as Windows packages
|
||||||
|
working-directory: ${{ env.DIOXUS_WORKSPACE }}/examples/01-app-demos/hotdog
|
||||||
|
run: cargo run --release --manifest-path ..\..\..\Cargo.toml -p dioxus-cli -- bundle --platform desktop --package-types msi --package-types nsis
|
||||||
|
- name: Verify Windows bundle outputs
|
||||||
|
working-directory: ${{ env.DIOXUS_WORKSPACE }}
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$msi = Get-ChildItem -Path .\target\dx -Recurse -File -Include "*.msi"
|
||||||
|
$nsis = Get-ChildItem -Path .\target\dx -Recurse -File -Include "*-setup.exe"
|
||||||
|
$msi | ForEach-Object { $_.FullName }
|
||||||
|
$nsis | ForEach-Object { $_.FullName }
|
||||||
|
if (-not $msi) { throw "Expected MSI artifact was not produced." }
|
||||||
|
if (-not $nsis) { throw "Expected NSIS artifact was not produced." }
|
||||||
|
- name: Target dir size by extension
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
Get-ChildItem -Path "$env:DIOXUS_WORKSPACE\target" -Recurse -File |
|
||||||
|
Group-Object Extension |
|
||||||
|
Sort-Object { ($_.Group | Measure-Object Length -Sum).Sum } -Descending |
|
||||||
|
Select-Object -First 20 @{N='Ext';E={$_.Name}}, Count, @{N='SizeMB';E={[math]::Round(($_.Group | Measure-Object Length -Sum).Sum / 1MB, 1)}} |
|
||||||
|
Format-Table -AutoSize
|
||||||
|
# - name: Clean Windows artifacts before cache save
|
||||||
|
# shell: pwsh
|
||||||
|
# run: |
|
||||||
|
# Get-ChildItem -Path "$env:DIOXUS_WORKSPACE\target" -Recurse -Include "*.pdb","*.exe","*.dll" | Remove-Item -Force
|
||||||
|
- uses: actions/upload-artifact@v6
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: hotdog-windows-bundles-smoke
|
||||||
|
path: |
|
||||||
|
${{ env.DIOXUS_WORKSPACE }}/target/**/*.msi
|
||||||
|
${{ env.DIOXUS_WORKSPACE }}/target/**/*-setup.exe
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
nix:
|
windows:
|
||||||
if: github.event.pull_request.draft == false
|
needs: check-paths
|
||||||
runs-on: ${{ matrix.os }}
|
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
|
||||||
strategy:
|
name: "Playwright | Windows"
|
||||||
matrix:
|
runs-on: warp-windows-2025-x64-8x
|
||||||
os: [blacksmith-4vcpu-ubuntu-2404, macos-latest]
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: nixbuild/nix-quick-install-action@master
|
- name: Setup Dev Drive
|
||||||
- uses: nix-community/cache-nix-action@main
|
run: ${{ github.workspace }}/.github/workflows/setup-dev-drive.ps1
|
||||||
|
- name: Copy Repo to Dev Drive
|
||||||
|
run: Copy-Item -Path "${{ github.workspace }}" -Destination "$env:DIOXUS_WORKSPACE" -Recurse
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
|
node-version: 24
|
||||||
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}
|
- name: Install Rust
|
||||||
- name: Install omnix
|
uses: dtolnay/rust-toolchain@1.88.0
|
||||||
run: nix --accept-flake-config profile install "github:juspay/omnix"
|
with:
|
||||||
- name: Build all flake outputs
|
targets: wasm32-unknown-unknown
|
||||||
run: om ci
|
components: rustfmt, clippy
|
||||||
- name: Ensure devShell has all build deps
|
- uses: Swatinem/rust-cache@v2
|
||||||
run: nix develop -c cargo build -p dioxus-cli --features no-downloads
|
with:
|
||||||
|
cache-all-crates: "true"
|
||||||
|
cache-workspace-crates: "true"
|
||||||
|
cache-provider: "warpbuild"
|
||||||
|
workspaces: ${{ env.DIOXUS_WORKSPACE }}
|
||||||
|
# save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
- name: Playwright
|
||||||
|
working-directory: ${{ env.DIOXUS_WORKSPACE }}/packages/playwright-tests
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 1
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm install -D @playwright/test
|
||||||
|
npx playwright install
|
||||||
|
npx playwright test
|
||||||
|
# - name: Clean Windows artifacts before cache save
|
||||||
|
# if: always()
|
||||||
|
# shell: pwsh
|
||||||
|
# run: |
|
||||||
|
# Get-ChildItem -Path "$env:DIOXUS_WORKSPACE\target" -Recurse -Include "*.pdb","*.exe","*.dll" | Remove-Item -Force
|
||||||
|
- uses: actions/upload-artifact@v6
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report-windows
|
||||||
|
path: ${{ env.DIOXUS_WORKSPACE }}/packages/playwright-tests/playwright-report/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
|
||||||
playwright:
|
playwright:
|
||||||
if: github.event.pull_request.draft == false
|
needs: check-paths
|
||||||
name: Playwright Tests
|
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
|
||||||
|
name: "Playwright | ${{ matrix.name }}"
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [blacksmith-4vcpu-windows-2025, blacksmith-8vcpu-ubuntu-2404]
|
include:
|
||||||
platform:
|
- { os: warp-ubuntu-latest-x64-8x, name: "Linux (stable)", toolchain: "1.88.0" }
|
||||||
- { toolchain: beta }
|
- { os: warp-ubuntu-latest-x64-8x, name: "Linux (beta)", toolchain: beta }
|
||||||
# - { toolchain: 1.86.0 }
|
|
||||||
steps:
|
steps:
|
||||||
# Do our best to cache the toolchain and node install steps
|
# Do our best to cache the toolchain and node install steps
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- name: Free Disk Space
|
- name: Free Disk Space
|
||||||
if: ${{ matrix.os == 'ubuntu-24.04' || matrix.os == 'ubuntu-24.04-arm' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404-arm' || matrix.os == 'blacksmith-8vcpu-ubuntu-2404' }}
|
if: runner.os == 'Linux'
|
||||||
uses: ./.github/actions/free-disk-space
|
uses: ./.github/actions/free-disk-space
|
||||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||||
if: ${{ matrix.os == 'ubuntu-24.04' || matrix.os == 'ubuntu-24.04-arm' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404-arm' || matrix.os == 'blacksmith-8vcpu-ubuntu-2404' }}
|
if: runner.os == 'Linux'
|
||||||
with:
|
with:
|
||||||
packages: glib-networking glib-networking-common glib-networking-services libaa1 libabsl20220623t64 libass9 libasyncns0 libavc1394-0 libavcodec60 libavfilter9 libavformat60 libavtp0 libavutil58 libblas3 libbluray2 libbs2b0 libcaca0 libcairo-script-interpreter2 libcdparanoia0 libchromaprint1 libcjson1 libcodec2-1.2 libdav1d7 libdc1394-25 libdca0 libdecor-0-0 libdirectfb-1.7-7t64 libdv4t64 libdvdnav4 libdvdread8t64 libfaad2 libflac12t64 libfluidsynth3 libfreeaptx0 libgav1-1 libgme0 libgraphene-1.0-0 libgsm1 libgssdp-1.6-0 libgstreamer-plugins-good1.0-0 libgtk-4-common libgupnp-1.6-0 libgupnp-igd-1.6-0 libhwy1t64 libiec61883-0 libimath-3-1-29t64 libinstpatch-1.0-2 libjack-jackd2-0 libjxl0.7 liblapack3 liblc3-1 libldacbt-enc2 liblilv-0-0 liblrdf0 libltc11 libmbedcrypto7t64 libmfx1 libmjpegutils-2.1-0t64 libmodplug1 libmp3lame0 libmpcdec6 libmpeg2encpp-2.1-0t64 libmpg123-0t64 libmplex2-2.1-0t64 libmysofa1 libneon27t64 libnice10 libogg0 libopenal-data libopenal1 libopenexr-3-1-30 libopenh264-7 libopenmpt0t64 libopenni2-0 liborc-0.4-0t64 libpipewire-0.3-0t64 libplacebo338 libpocketsphinx3 libpostproc57 libproxy1v5 libpulse0 libqrencode4 libraptor2-0 librav1e0 libraw1394-11 librist4 librubberband2 libsamplerate0 libsbc1 libsdl2-2.0-0 libsecret-common libserd-0-0 libshine3 libshout3 libsndfile1 libsndio7.0 libsord-0-0 libsoundtouch1 libsoup-3.0-0 libsoup-3.0-common libsoxr0 libspa-0.2-modules libspandsp2t64 libspeex1 libsphinxbase3t64 libsratom-0-0 libsrt1.5-gnutls libsrtp2-1 libssh-gcrypt-4 libsvtav1enc1d1 libswresample4 libswscale7 libtag1v5 libtag1v5-vanilla libtheora0 libtwolame0 libudfread0 libunibreak5 libv4l-0t64 libv4lconvert0t64 libva-drm2 libva-x11-2 libva2 libvdpau1 libvidstab1.1 libvisual-0.4-0 libvo-aacenc0 libvo-amrwbenc0 libvorbis0a libvorbisenc2 libvorbisfile3 libvpl2 libwavpack1 libwebrtc-audio-processing1 libwildmidi2 libx265-199 libxcb-xkb1 libxkbcommon-x11-0 libcups2t64 libxml2 libxml2-dev libxvidcore4 libyuv0 libzbar0t64 libzimg2 libzix-0-0 libzvbi-common libzvbi0t64 libzxing3 ocl-icd-libopencl1 timgm6mb-soundfont xfonts-encodings xfonts-utils binutils lld binutils-devel binutils-gold fonts-freefont-ttf fonts-ipafont-gothic fonts-tlwg-loma-otf fonts-unifont fonts-wqy-zenhei gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-good libavif16 libevent-2.1-7t64 libgstreamer-plugins-bad1.0-0 libharfbuzz-icu0 libhyphen0 libmanette-0.2-0 libsecret-1-0 libwoff1 xfonts-cyrillic xfonts-scalable fonts-ipafont-mincho fonts-tlwg-loma gstreamer1.0-x
|
packages: glib-networking glib-networking-common glib-networking-services libaa1 libabsl20220623t64 libass9 libasyncns0 libavc1394-0 libavcodec60 libavfilter9 libavformat60 libavtp0 libavutil58 libblas3 libbluray2 libbs2b0 libcaca0 libcairo-script-interpreter2 libcdparanoia0 libchromaprint1 libcjson1 libcodec2-1.2 libdav1d7 libdc1394-25 libdca0 libdecor-0-0 libdirectfb-1.7-7t64 libdv4t64 libdvdnav4 libdvdread8t64 libfaad2 libflac12t64 libfluidsynth3 libfreeaptx0 libgav1-1 libgme0 libgraphene-1.0-0 libgsm1 libgssdp-1.6-0 libgstreamer-plugins-good1.0-0 libgtk-4-common libgupnp-1.6-0 libgupnp-igd-1.6-0 libhwy1t64 libiec61883-0 libimath-3-1-29t64 libinstpatch-1.0-2 libjack-jackd2-0 libjxl0.7 liblapack3 liblc3-1 libldacbt-enc2 liblilv-0-0 liblrdf0 libltc11 libmbedcrypto7t64 libmfx1 libmjpegutils-2.1-0t64 libmodplug1 libmp3lame0 libmpcdec6 libmpeg2encpp-2.1-0t64 libmpg123-0t64 libmplex2-2.1-0t64 libmysofa1 libneon27t64 libnice10 libogg0 libopenal-data libopenal1 libopenexr-3-1-30 libopenh264-7 libopenmpt0t64 libopenni2-0 liborc-0.4-0t64 libpipewire-0.3-0t64 libplacebo338 libpocketsphinx3 libpostproc57 libproxy1v5 libpulse0 libqrencode4 libraptor2-0 librav1e0 libraw1394-11 librist4 librubberband2 libsamplerate0 libsbc1 libsdl2-2.0-0 libsecret-common libserd-0-0 libshine3 libshout3 libsndfile1 libsndio7.0 libsord-0-0 libsoundtouch1 libsoup-3.0-0 libsoup-3.0-common libsoxr0 libspa-0.2-modules libspandsp2t64 libspeex1 libsphinxbase3t64 libsratom-0-0 libsrt1.5-gnutls libsrtp2-1 libssh-gcrypt-4 libsvtav1enc1d1 libswresample4 libswscale7 libtag1v5 libtag1v5-vanilla libtheora0 libtwolame0 libudfread0 libunibreak5 libv4l-0t64 libv4lconvert0t64 libva-drm2 libva-x11-2 libva2 libvdpau1 libvidstab1.1 libvisual-0.4-0 libvo-aacenc0 libvo-amrwbenc0 libvorbis0a libvorbisenc2 libvorbisfile3 libvpl2 libwavpack1 libwebrtc-audio-processing1 libwildmidi2 libx265-199 libxcb-xkb1 libxkbcommon-x11-0 libcups2t64 libxml2 libxml2-dev libxvidcore4 libyuv0 libzbar0t64 libzimg2 libzix-0-0 libzvbi-common libzvbi0t64 libzxing3 ocl-icd-libopencl1 timgm6mb-soundfont xfonts-encodings xfonts-utils binutils lld binutils-devel binutils-gold fonts-freefont-ttf fonts-ipafont-gothic fonts-tlwg-loma-otf fonts-unifont fonts-wqy-zenhei gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-good libavif16 libevent-2.1-7t64 libgstreamer-plugins-bad1.0-0 libharfbuzz-icu0 libhyphen0 libmanette-0.2-0 libsecret-1-0 libwoff1 xfonts-cyrillic xfonts-scalable fonts-ipafont-mincho fonts-tlwg-loma gstreamer1.0-x
|
||||||
version: 1.0
|
version: 1.0
|
||||||
@@ -264,15 +386,17 @@ jobs:
|
|||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ matrix.platform.toolchain }}
|
toolchain: ${{ matrix.toolchain }}
|
||||||
targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown
|
targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
key: "playwright-${{ matrix.platform.toolchain }}-${{ runner.os }}"
|
key: "playwright-${{ matrix.toolchain }}-${{ runner.os }}"
|
||||||
cache-all-crates: "true"
|
cache-all-crates: "true"
|
||||||
cache-on-failure: "true"
|
cache-workspace-crates: "true"
|
||||||
|
cache-provider: "warpbuild"
|
||||||
|
# save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
- name: Wipe dx cache
|
- name: Wipe dx cache
|
||||||
if: ${{ matrix.os == 'ubuntu-24.04' || matrix.os == 'ubuntu-24.04-arm' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.os == 'blacksmith-4vcpu-ubuntu-2404-arm' || matrix.os == 'blacksmith-8vcpu-ubuntu-2404' }}
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
rm -rf ./target/dx
|
rm -rf ./target/dx
|
||||||
- name: Playwright
|
- name: Playwright
|
||||||
@@ -288,13 +412,67 @@ jobs:
|
|||||||
- uses: actions/upload-artifact@v6
|
- uses: actions/upload-artifact@v6
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: playwright-report-${{ matrix.platform.toolchain }}-${{ runner.os }}
|
name: playwright-report-${{ matrix.toolchain }}-${{ runner.os }}
|
||||||
path: ./packages/playwright-tests/playwright-report/
|
path: ./packages/playwright-tests/playwright-report/
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test:
|
||||||
|
needs: check-paths
|
||||||
|
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
|
||||||
|
name: "Test | Linux"
|
||||||
|
runs-on: warp-ubuntu-latest-x64-8x
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- name: Free Disk Space
|
||||||
|
uses: ./.github/actions/free-disk-space
|
||||||
|
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||||
|
with:
|
||||||
|
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
|
||||||
|
version: 1.0
|
||||||
|
- uses: dtolnay/rust-toolchain@1.88.0
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
cache-all-crates: "true"
|
||||||
|
cache-workspace-crates: "true"
|
||||||
|
cache-provider: "warpbuild"
|
||||||
|
# save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
- uses: browser-actions/setup-firefox@latest
|
||||||
|
- run: cargo test --lib --bins --tests --examples --workspace --exclude dioxus-desktop
|
||||||
|
|
||||||
|
release-test:
|
||||||
|
needs: check-paths
|
||||||
|
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
|
||||||
|
name: "Test | Linux (Release)"
|
||||||
|
runs-on: warp-ubuntu-latest-x64-8x
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- name: Free Disk Space
|
||||||
|
uses: ./.github/actions/free-disk-space
|
||||||
|
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||||
|
with:
|
||||||
|
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
|
||||||
|
version: 1.0
|
||||||
|
- uses: dtolnay/rust-toolchain@1.88.0
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
cache-all-crates: "true"
|
||||||
|
cache-workspace-crates: "true"
|
||||||
|
cache-provider: "warpbuild"
|
||||||
|
# save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
- uses: browser-actions/setup-firefox@latest
|
||||||
|
- run: cargo test --lib --bins --tests --examples --workspace --exclude dioxus-desktop --profile release-unoptimized
|
||||||
|
|
||||||
matrix_test:
|
matrix_test:
|
||||||
|
needs: check-paths
|
||||||
runs-on: ${{ matrix.platform.os }}
|
runs-on: ${{ matrix.platform.os }}
|
||||||
if: github.event.pull_request.draft == false
|
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
|
||||||
|
name: "Cross | ${{ matrix.platform.target }}"
|
||||||
env:
|
env:
|
||||||
RUST_CARGO_COMMAND: ${{ matrix.platform.cross == true && 'cross' || 'cargo' }}
|
RUST_CARGO_COMMAND: ${{ matrix.platform.cross == true && 'cross' || 'cargo' }}
|
||||||
strategy:
|
strategy:
|
||||||
@@ -302,7 +480,7 @@ jobs:
|
|||||||
platform:
|
platform:
|
||||||
- {
|
- {
|
||||||
target: aarch64-apple-darwin,
|
target: aarch64-apple-darwin,
|
||||||
os: macos-latest,
|
os: warp-macos-latest-arm64-6x,
|
||||||
toolchain: "1.88.0",
|
toolchain: "1.88.0",
|
||||||
cross: false,
|
cross: false,
|
||||||
command: "test",
|
command: "test",
|
||||||
@@ -311,7 +489,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
target: aarch64-apple-ios,
|
target: aarch64-apple-ios,
|
||||||
os: macos-latest,
|
os: warp-macos-latest-arm64-6x,
|
||||||
toolchain: "1.88.0",
|
toolchain: "1.88.0",
|
||||||
cross: false,
|
cross: false,
|
||||||
command: "build",
|
command: "build",
|
||||||
@@ -320,7 +498,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
target: aarch64-unknown-linux-gnu,
|
target: aarch64-unknown-linux-gnu,
|
||||||
os: ubuntu-24.04-arm,
|
os: warp-ubuntu-latest-arm64-4x,
|
||||||
toolchain: "1.88.0",
|
toolchain: "1.88.0",
|
||||||
cross: false,
|
cross: false,
|
||||||
command: "build",
|
command: "build",
|
||||||
@@ -329,14 +507,14 @@ jobs:
|
|||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
target: aarch64-linux-android,
|
target: aarch64-linux-android,
|
||||||
os: blacksmith-4vcpu-ubuntu-2404,
|
os: warp-ubuntu-latest-x64-4x,
|
||||||
toolchain: "1.88.0",
|
toolchain: "1.88.0",
|
||||||
cross: true,
|
cross: true,
|
||||||
command: "build",
|
command: "build",
|
||||||
args: "--package dioxus-desktop",
|
args: "--package dioxus-desktop",
|
||||||
platform: "android",
|
platform: "android",
|
||||||
}
|
}
|
||||||
# commented out because it's having issues with space on the device, but we already test it above
|
# commented out because it's having issues with space on the device, but we already test it above
|
||||||
# - {
|
# - {
|
||||||
# target: x86_64-unknown-linux-gnu,
|
# target: x86_64-unknown-linux-gnu,
|
||||||
# os: ubuntu-24.04,
|
# os: ubuntu-24.04,
|
||||||
@@ -350,15 +528,31 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- name: Free Disk Space
|
- name: Free Disk Space
|
||||||
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404-arm' }}
|
if: runner.os == 'Linux'
|
||||||
uses: ./.github/actions/free-disk-space
|
uses: ./.github/actions/free-disk-space
|
||||||
|
|
||||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||||
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404' || matrix.platform.os == 'blacksmith-4vcpu-ubuntu-2404-arm' }}
|
if: ${{ runner.os == 'Linux' && matrix.platform.target != 'aarch64-unknown-linux-gnu' }}
|
||||||
with:
|
with:
|
||||||
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
|
packages: pkg-config libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
|
||||||
version: ${{ matrix.platform.target }}-${{ matrix.platform.os }} # disambiguate since we're in a matrix and this caching action doesn't factor in these variables
|
version: ${{ matrix.platform.target }}-${{ matrix.platform.os }} # disambiguate since we're in a matrix and this caching action doesn't factor in these variables
|
||||||
|
|
||||||
|
- name: Install Linux desktop dependencies for ARM
|
||||||
|
if: ${{ matrix.platform.target == 'aarch64-unknown-linux-gnu' }}
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y pkg-config libwebkit2gtk-4.1-dev libgtk-3-dev libasound2-dev libudev-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev
|
||||||
|
|
||||||
|
- name: Verify GLib pkg-config metadata on ARM Linux
|
||||||
|
if: ${{ matrix.platform.target == 'aarch64-unknown-linux-gnu' }}
|
||||||
|
run: pkg-config --modversion glib-2.0
|
||||||
|
|
||||||
|
- name: Configure pkg-config for ARM Linux desktop deps
|
||||||
|
if: ${{ matrix.platform.target == 'aarch64-unknown-linux-gnu' }}
|
||||||
|
run: |
|
||||||
|
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig" >> "$GITHUB_ENV"
|
||||||
|
echo "PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: install stable
|
- name: install stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
@@ -366,7 +560,7 @@ jobs:
|
|||||||
targets: ${{ matrix.platform.target }}
|
targets: ${{ matrix.platform.target }}
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
|
|
||||||
- name: Install nasm for windows (tls)
|
- name: Install nasm for windows (tls)1
|
||||||
if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }}
|
if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }}
|
||||||
uses: ilammy/setup-nasm@v1
|
uses: ilammy/setup-nasm@v1
|
||||||
|
|
||||||
@@ -378,33 +572,45 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
key: "matrix-${{ matrix.platform.target }}"
|
key: "matrix-${{ matrix.platform.target }}"
|
||||||
cache-all-crates: "true"
|
cache-all-crates: "true"
|
||||||
|
cache-workspace-crates: "true"
|
||||||
|
cache-provider: "warpbuild"
|
||||||
|
# save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
run: |
|
run: |
|
||||||
${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}
|
${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}
|
||||||
|
|
||||||
# borrowed from uv
|
nix:
|
||||||
# https://raw.githubusercontent.com/astral-sh/uv/refs/heads/main/.github/workflows/ci.yml
|
needs: check-paths
|
||||||
cargo-test-windows:
|
if: github.event.pull_request.draft == false && needs.check-paths.outputs.rust == 'true'
|
||||||
if: github.event.pull_request.draft == false
|
name: "Nix | ${{ matrix.name }}"
|
||||||
runs-on:
|
runs-on: ${{ matrix.os }}
|
||||||
labels: "blacksmith-4vcpu-windows-2025"
|
strategy:
|
||||||
name: "cargo test | windows"
|
matrix:
|
||||||
|
include:
|
||||||
|
- { os: warp-ubuntu-latest-x64-8x, name: Linux }
|
||||||
|
- { os: warp-macos-latest-arm64-6x, name: macOS }
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: dtolnay/rust-toolchain@1.88.0
|
- uses: nixbuild/nix-quick-install-action@master
|
||||||
|
- uses: jkelleyrtp/cache-nix-action@jk/use-warpbuild
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.nix', 'flake.lock') }}-${{ hashFiles('Cargo.lock') }}
|
||||||
- uses: Swatinem/rust-cache@v2
|
restore-prefixes-first-match: |
|
||||||
with:
|
nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.nix', 'flake.lock') }}-
|
||||||
workspaces: ${{ env.UV_WORKSPACE }}
|
nix-${{ runner.os }}-${{ runner.arch }}-
|
||||||
cache-all-crates: "true"
|
backend: warpbuild
|
||||||
|
- name: Build dioxus-cli from the flake
|
||||||
- name: "Install Rust toolchain"
|
run: nix build .#dioxus-cli
|
||||||
working-directory: ${{ env.UV_WORKSPACE }}
|
- name: Smoke test devShell
|
||||||
run: rustup show
|
shell: bash
|
||||||
|
|
||||||
- name: "Cargo test"
|
|
||||||
working-directory: ${{ env.UV_WORKSPACE }}
|
|
||||||
run: |
|
run: |
|
||||||
cargo test --workspace --tests
|
nix develop .#default -c bash -lc '
|
||||||
|
cargo --version
|
||||||
|
rustc --version
|
||||||
|
pkg-config --modversion openssl
|
||||||
|
if [[ "${{ runner.os }}" == "Linux" ]]; then
|
||||||
|
pkg-config --modversion gtk+-3.0
|
||||||
|
pkg-config --modversion webkit2gtk-4.1
|
||||||
|
fi
|
||||||
|
'
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
# Attempt to backport a merged pull request to the latest stable release
|
# Attempt to backport a merged pull request to the latest stable release
|
||||||
backport:
|
backport:
|
||||||
name: Backport pull request
|
name: Backport pull request
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: warp-ubuntu-latest-x64-4x
|
||||||
|
|
||||||
# Don't run on closed unmerged pull requests, or pull requests with the "breaking" label
|
# Don't run on closed unmerged pull requests, or pull requests with the "breaking" label
|
||||||
if: github.event.pull_request.merged && !contains(github.event.pull_request.labels.*.name, 'breaking')
|
if: github.event.pull_request.merged && !contains(github.event.pull_request.labels.*.name, 'breaking')
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
# Upload nightly docs to the website
|
# Upload nightly docs to the website
|
||||||
docs:
|
docs:
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: warp-ubuntu-latest-x64-4x
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
@@ -47,6 +47,7 @@ jobs:
|
|||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
cache-all-crates: "true"
|
cache-all-crates: "true"
|
||||||
|
cache-provider: "warpbuild"
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: cargo doc
|
- name: cargo doc
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
promote:
|
promote:
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: warp-ubuntu-latest-x64-4x
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- name: Publish the next pre-release
|
- name: Publish the next pre-release
|
||||||
|
|||||||
@@ -84,11 +84,21 @@ jobs:
|
|||||||
uses: ./.github/actions/free-disk-space
|
uses: ./.github/actions/free-disk-space
|
||||||
|
|
||||||
- uses: awalsh128/cache-apt-pkgs-action@latest
|
- uses: awalsh128/cache-apt-pkgs-action@latest
|
||||||
if: ${{ matrix.platform.os == 'ubuntu-24.04' || matrix.platform.os == 'ubuntu-24.04-arm' }}
|
if: ${{ matrix.platform.os == 'ubuntu-24.04' }}
|
||||||
with:
|
with:
|
||||||
packages: libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev musl-tools
|
packages: pkg-config libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev musl-tools
|
||||||
version: 1.0
|
version: 1.0
|
||||||
|
|
||||||
|
- name: Install Linux desktop dependencies for ARM
|
||||||
|
if: ${{ matrix.platform.os == 'ubuntu-24.04-arm' }}
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y pkg-config libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev libxdo-dev libglib2.0-dev musl-tools
|
||||||
|
|
||||||
|
- name: Verify GLib pkg-config metadata on ARM Linux
|
||||||
|
if: ${{ matrix.platform.os == 'ubuntu-24.04-arm' }}
|
||||||
|
run: pkg-config --modversion glib-2.0
|
||||||
|
|
||||||
- name: Install stable
|
- name: Install stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -1,26 +1,74 @@
|
|||||||
# This creates a 20GB dev drive, and exports all required environment
|
# Configures a dev drive for Windows CI to speed up Rust builds.
|
||||||
# variables so that rustup, uv and others all use the dev drive as much
|
#
|
||||||
# as possible.
|
# The main performance win is disabling Windows Defender antivirus scanning
|
||||||
$Volume = New-VHD -Path C:/uv_dev_drive.vhdx -SizeBytes 20GB |
|
# on the build drive — AV scanning every .o, .rlib, .rmeta, and .exe during
|
||||||
Mount-VHD -Passthru |
|
# a Rust build is extremely expensive.
|
||||||
Initialize-Disk -Passthru |
|
#
|
||||||
New-Partition -AssignDriveLetter -UseMaximumSize |
|
# Three code paths:
|
||||||
Format-Volume -FileSystem ReFS -Confirm:$false -Force
|
# 1. D: drive exists (some runners) — use it directly
|
||||||
|
# 2. Hyper-V available — create a Dev Drive VHD (best perf, supports fsutil devdrv)
|
||||||
|
# 3. No Hyper-V (Warp/EC2 runners) — use diskpart + ReFS format
|
||||||
|
|
||||||
Write-Output $Volume
|
if (Test-Path "D:\") {
|
||||||
|
Write-Output "Using existing drive at D:"
|
||||||
|
$Drive = "D:"
|
||||||
|
} elseif (Get-Command New-VHD -ErrorAction SilentlyContinue) {
|
||||||
|
# Hyper-V is available — create a proper Dev Drive
|
||||||
|
$Volume = New-VHD -Path C:/dev_drive.vhdx -SizeBytes 25GB |
|
||||||
|
Mount-VHD -Passthru |
|
||||||
|
Initialize-Disk -Passthru |
|
||||||
|
New-Partition -AssignDriveLetter -UseMaximumSize |
|
||||||
|
Format-Volume -DevDrive -Confirm:$false -Force
|
||||||
|
|
||||||
$Drive = "$($Volume.DriveLetter):"
|
$Drive = "$($Volume.DriveLetter):"
|
||||||
$Tmp = "$($Drive)/uv-tmp"
|
|
||||||
|
|
||||||
# Create the directory ahead of time in an attempt to avoid race-conditions
|
# Mark as trusted and disable antivirus filtering
|
||||||
New-Item $Tmp -ItemType Directory
|
fsutil devdrv trust $Drive
|
||||||
|
fsutil devdrv enable /disallowAv
|
||||||
|
|
||||||
|
# Remount so the changes take effect
|
||||||
|
Dismount-VHD -Path C:/dev_drive.vhdx
|
||||||
|
Mount-VHD -Path C:/dev_drive.vhdx
|
||||||
|
|
||||||
|
Write-Output $Volume
|
||||||
|
fsutil devdrv query $Drive
|
||||||
|
Write-Output "Created Dev Drive at $Drive"
|
||||||
|
} else {
|
||||||
|
# No Hyper-V — fall back to diskpart + ReFS
|
||||||
|
Write-Output "No Hyper-V detected, creating ReFS drive via diskpart..."
|
||||||
|
|
||||||
|
$vhdPath = "C:\dev_drive.vhdx"
|
||||||
|
@"
|
||||||
|
create vdisk file="$vhdPath" maximum=25600 type=expandable
|
||||||
|
attach vdisk
|
||||||
|
create partition primary
|
||||||
|
active
|
||||||
|
assign letter=V
|
||||||
|
"@ | diskpart
|
||||||
|
|
||||||
|
format V: /fs:ReFS /q /y
|
||||||
|
$Drive = "V:"
|
||||||
|
|
||||||
|
Write-Output "Created ReFS drive at $Drive"
|
||||||
|
}
|
||||||
|
|
||||||
|
$Tmp = "$($Drive)\tmp"
|
||||||
|
|
||||||
|
# Create directories ahead of time to avoid race conditions
|
||||||
|
New-Item $Tmp -ItemType Directory -Force
|
||||||
|
|
||||||
|
# Move Cargo to the dev drive so registry/crate downloads also benefit
|
||||||
|
New-Item -Path "$($Drive)/.cargo/bin" -ItemType Directory -Force
|
||||||
|
if (Test-Path "C:/Users/runneradmin/.cargo") {
|
||||||
|
Copy-Item -Path "C:/Users/runneradmin/.cargo/*" -Destination "$($Drive)/.cargo/" -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
Write-Output `
|
Write-Output `
|
||||||
"DEV_DRIVE=$($Drive)" `
|
"DEV_DRIVE=$($Drive)" `
|
||||||
"TMP=$($Tmp)" `
|
"TMP=$($Tmp)" `
|
||||||
"TEMP=$($Tmp)" `
|
"TEMP=$($Tmp)" `
|
||||||
"RUSTUP_HOME=$($Drive)/.rustup" `
|
"RUSTUP_HOME=$($Drive)/.rustup" `
|
||||||
"CARGO_HOME=$($Drive)/.cargo" `
|
"CARGO_HOME=$($Drive)/.cargo" `
|
||||||
"UV_WORKSPACE=$($Drive)/uv" `
|
"DIOXUS_WORKSPACE=$($Drive)/dioxus" `
|
||||||
"PATH=$($Drive)/.cargo/bin;$env:PATH" `
|
"PATH=$($Drive)/.cargo/bin;$env:PATH" `
|
||||||
>> $env:GITHUB_ENV
|
>> $env:GITHUB_ENV
|
||||||
@@ -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",
|
||||||
"packages/core-types",
|
"packages/core-types",
|
||||||
"packages/cli",
|
"packages/cli",
|
||||||
"packages/cli-opt",
|
|
||||||
"packages/cli-config",
|
"packages/cli-config",
|
||||||
"packages/cli-telemetry",
|
"packages/cli-telemetry",
|
||||||
"packages/core-macro",
|
"packages/core-macro",
|
||||||
@@ -22,7 +21,6 @@ members = [
|
|||||||
"packages/interpreter",
|
"packages/interpreter",
|
||||||
"packages/liveview",
|
"packages/liveview",
|
||||||
"packages/autofmt",
|
"packages/autofmt",
|
||||||
"packages/check",
|
|
||||||
"packages/devtools-types",
|
"packages/devtools-types",
|
||||||
"packages/devtools",
|
"packages/devtools",
|
||||||
"packages/document",
|
"packages/document",
|
||||||
@@ -140,7 +138,6 @@ dioxus-desktop = { path = "packages/desktop", version = "0.7.3", default-feature
|
|||||||
dioxus-interpreter-js = { path = "packages/interpreter", version = "0.7.3" }
|
dioxus-interpreter-js = { path = "packages/interpreter", version = "0.7.3" }
|
||||||
dioxus-liveview = { path = "packages/liveview", version = "0.7.3" }
|
dioxus-liveview = { path = "packages/liveview", version = "0.7.3" }
|
||||||
dioxus-autofmt = { path = "packages/autofmt", version = "0.7.3" }
|
dioxus-autofmt = { path = "packages/autofmt", version = "0.7.3" }
|
||||||
dioxus-check = { path = "packages/check", version = "0.7.3" }
|
|
||||||
dioxus-rsx = { path = "packages/rsx", version = "0.7.3" }
|
dioxus-rsx = { path = "packages/rsx", version = "0.7.3" }
|
||||||
dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.7.3" }
|
dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.7.3" }
|
||||||
dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.7.3" }
|
dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.7.3" }
|
||||||
@@ -164,7 +161,6 @@ generational-box = { path = "packages/generational-box", version = "0.7.3" }
|
|||||||
lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.7.3" }
|
lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.7.3" }
|
||||||
|
|
||||||
# cli
|
# cli
|
||||||
dioxus-cli-opt = { path = "packages/cli-opt", version = "0.7.3" }
|
|
||||||
dioxus-cli-telemetry = { path = "packages/cli-telemetry", version = "0.7.3" }
|
dioxus-cli-telemetry = { path = "packages/cli-telemetry", version = "0.7.3" }
|
||||||
dioxus-cli-config = { path = "packages/cli-config", version = "0.7.3" }
|
dioxus-cli-config = { path = "packages/cli-config", version = "0.7.3" }
|
||||||
|
|
||||||
@@ -229,7 +225,7 @@ futures = "0.3.31"
|
|||||||
futures-channel = "0.3.31"
|
futures-channel = "0.3.31"
|
||||||
futures-util = { version = "0.3", default-features = false }
|
futures-util = { version = "0.3", default-features = false }
|
||||||
rustc-hash = "2.1.1"
|
rustc-hash = "2.1.1"
|
||||||
wasm-bindgen = "0.2.105"
|
wasm-bindgen = "0.2.114"
|
||||||
wasm-bindgen-futures = "0.4.50"
|
wasm-bindgen-futures = "0.4.50"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
web-sys = { version = "0.3.77", default-features = false }
|
web-sys = { version = "0.3.77", default-features = false }
|
||||||
@@ -238,9 +234,12 @@ thiserror = "2.0.12"
|
|||||||
prettyplease = { version = "0.2.35", features = ["verbatim"] }
|
prettyplease = { version = "0.2.35", features = ["verbatim"] }
|
||||||
const_format = "0.2.34"
|
const_format = "0.2.34"
|
||||||
cargo_toml = { version = "0.22.1" }
|
cargo_toml = { version = "0.22.1" }
|
||||||
tauri-utils = { version = "=2.5.0" }
|
icns = "0.4.0"
|
||||||
tauri-macos-sign = { version = "=2.2.0" }
|
image = "0.25"
|
||||||
tauri-bundler = { version = "=2.5.0" }
|
zip = { version = "4", default-features = false, features = ["deflate"] }
|
||||||
|
sha1 = "0.10"
|
||||||
|
md5 = "0.8.0"
|
||||||
|
rpm = { version = "0.19.0", default-features = false, features = ["gzip-compression"] }
|
||||||
lru = "0.16.0"
|
lru = "0.16.0"
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.88"
|
||||||
axum = { version = "0.8.4", default-features = false }
|
axum = { version = "0.8.4", default-features = false }
|
||||||
@@ -282,8 +281,8 @@ ciborium = "0.2.2"
|
|||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
uuid = "1.17.0"
|
uuid = "1.17.0"
|
||||||
convert_case = "0.8.0"
|
convert_case = "0.8.0"
|
||||||
tungstenite = { version = "0.27.0" }
|
tungstenite = { version = "0.28.0" }
|
||||||
tokio-tungstenite = { version = "0.27.0" }
|
tokio-tungstenite = { version = "0.28.0" }
|
||||||
gloo-timers = "0.3.0"
|
gloo-timers = "0.3.0"
|
||||||
internment = { version = "0.8.6" }
|
internment = { version = "0.8.6" }
|
||||||
proc-macro2-diagnostics = { version = "0.10", default-features = false }
|
proc-macro2-diagnostics = { version = "0.10", default-features = false }
|
||||||
@@ -359,7 +358,7 @@ gloo-dialogs = "0.2.0"
|
|||||||
ansi-to-tui = "7.0"
|
ansi-to-tui = "7.0"
|
||||||
ansi-to-html = "0.2.2"
|
ansi-to-html = "0.2.2"
|
||||||
path-absolutize = "3.1"
|
path-absolutize = "3.1"
|
||||||
crossterm = { version = "0.29.0" }
|
crossterm = { version = "0.28.1" }
|
||||||
ratatui = { version = "0.29.0" }
|
ratatui = { version = "0.29.0" }
|
||||||
shell-words = "1.1.0"
|
shell-words = "1.1.0"
|
||||||
|
|
||||||
@@ -367,6 +366,11 @@ shell-words = "1.1.0"
|
|||||||
keyboard-types = { version = "0.7", default-features = false }
|
keyboard-types = { version = "0.7", default-features = false }
|
||||||
winit = { version = "0.30.11", features = ["rwh_06"] }
|
winit = { version = "0.30.11", features = ["rwh_06"] }
|
||||||
|
|
||||||
|
# reduce debug symbols to line tables only - speeds up compiles and shrinks artifacts
|
||||||
|
# backtraces still show file:line, but debugger can't inspect variables
|
||||||
|
[profile.dev]
|
||||||
|
debug = "line-tables-only"
|
||||||
|
|
||||||
# our release profile should be fast to compile and fast to run
|
# our release profile should be fast to compile and fast to run
|
||||||
# when we ship our CI builds, we turn on LTO which improves perf leftover by turning on incremental
|
# when we ship our CI builds, we turn on LTO which improves perf leftover by turning on incremental
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ seeked = "seeked"
|
|||||||
udid = "udid"
|
udid = "udid"
|
||||||
# Part of Blitz's API
|
# Part of Blitz's API
|
||||||
unparented = "unparented"
|
unparented = "unparented"
|
||||||
|
# bundle type / archive
|
||||||
|
flate2="flate2"
|
||||||
|
|
||||||
[files]
|
[files]
|
||||||
extend-exclude = ["notes/translations/*", "CHANGELOG.md", "*.js"]
|
extend-exclude = ["notes/translations/*", "CHANGELOG.md", "*.js"]
|
||||||
@@ -10,7 +10,7 @@ dioxus = { workspace = true, features = ["fullstack", "router"] }
|
|||||||
reqwest = { workspace = true, features = ["json"] }
|
reqwest = { workspace = true, features = ["json"] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
rusqlite = { version = "0.32.0", optional = true }
|
rusqlite = { version = "0.32.0", optional = true, features = ["bundled"] } # Bundle SQLite so Windows/MSVC builds do not require an external sqlite3.lib.
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
Generated
+16
@@ -1,5 +1,20 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1773189535,
|
||||||
|
"narHash": "sha256-E1G/Or6MWeP+L6mpQ0iTFLpzSzlpGrITfU2220Gq47g=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "6fa2fb4cf4a89ba49fc9dd5a3eb6cde99d388269",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"flake-parts": {
|
"flake-parts": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs-lib": "nixpkgs-lib"
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
@@ -67,6 +82,7 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
"flake-parts": "flake-parts",
|
"flake-parts": "flake-parts",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"rust-overlay": "rust-overlay",
|
"rust-overlay": "rust-overlay",
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
systems.url = "github:nix-systems/default";
|
systems.url = "github:nix-systems/default";
|
||||||
|
|
||||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
# crane.url = "github:ipetkov/crane";
|
crane.url = "github:ipetkov/crane";
|
||||||
# crane.inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
@@ -16,14 +15,18 @@
|
|||||||
|
|
||||||
perSystem =
|
perSystem =
|
||||||
{
|
{
|
||||||
config,
|
|
||||||
self',
|
self',
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
system,
|
system,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
pkgs = import inputs.nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [
|
||||||
|
inputs.rust-overlay.overlays.default
|
||||||
|
];
|
||||||
|
};
|
||||||
|
lib = pkgs.lib;
|
||||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||||
extensions = [
|
extensions = [
|
||||||
"rust-src"
|
"rust-src"
|
||||||
@@ -31,6 +34,7 @@
|
|||||||
"clippy"
|
"clippy"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||||
rustBuildInputs = [
|
rustBuildInputs = [
|
||||||
pkgs.openssl
|
pkgs.openssl
|
||||||
pkgs.libiconv
|
pkgs.libiconv
|
||||||
@@ -49,6 +53,26 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
||||||
|
fullSrc = pkgs.lib.cleanSource ./.;
|
||||||
|
cargoSrc = craneLib.cleanCargoSource fullSrc;
|
||||||
|
|
||||||
|
commonArgs = {
|
||||||
|
src = fullSrc;
|
||||||
|
strictDeps = true;
|
||||||
|
buildInputs = rustBuildInputs;
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.pkg-config
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoArtifacts = craneLib.buildDepsOnly (
|
||||||
|
commonArgs
|
||||||
|
// {
|
||||||
|
src = cargoSrc;
|
||||||
|
pname = "dioxus-deps";
|
||||||
|
version = cargoToml.package.version;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
rustPackage =
|
rustPackage =
|
||||||
package:
|
package:
|
||||||
@@ -56,53 +80,32 @@
|
|||||||
binary ? package,
|
binary ? package,
|
||||||
features ? [ ],
|
features ? [ ],
|
||||||
}:
|
}:
|
||||||
(pkgs.makeRustPlatform {
|
craneLib.buildPackage (
|
||||||
cargo = rustToolchain;
|
commonArgs
|
||||||
rustc = rustToolchain;
|
// {
|
||||||
}).buildRustPackage
|
|
||||||
{
|
|
||||||
pname = package;
|
pname = package;
|
||||||
version = cargoToml.package.version;
|
version = cargoToml.package.version;
|
||||||
src = pkgs.lib.cleanSource ./.;
|
inherit cargoArtifacts;
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoExtraArgs = "--locked --package ${package} ${
|
||||||
buildInputs = rustBuildInputs;
|
lib.concatStringsSep " " (map (f: "--features ${f}") features)
|
||||||
nativeBuildInputs = [
|
}";
|
||||||
rustToolchain
|
doCheck = false; # Disable tests to avoid building deps for them
|
||||||
pkgs.pkg-config
|
installPhaseCommand = ''
|
||||||
];
|
|
||||||
buildPhase = ''
|
|
||||||
mkdir -p .cargo
|
|
||||||
cp ${./Cargo.lock} Cargo.lock
|
|
||||||
cargo build --release --package ${package} ${
|
|
||||||
lib.concatStringsSep " " (map (f: "--features ${f}") features)
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
installPhase = ''
|
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin
|
||||||
ls -alR target/release
|
|
||||||
cp target/release/${binary} $out/bin/
|
cp target/release/${binary} $out/bin/
|
||||||
'';
|
'';
|
||||||
doCheck = false; # Disable tests to avoid building deps for them
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
# This is useful when building crates as packages
|
|
||||||
# Note that it does require a `Cargo.lock` which this repo does not have
|
|
||||||
# craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
_module.args.pkgs = import inputs.nixpkgs {
|
|
||||||
inherit system;
|
|
||||||
overlays = [
|
|
||||||
inputs.rust-overlay.overlays.default
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
packages.dioxus-cli = (
|
packages.dioxus-cli = (
|
||||||
rustPackage "dioxus-cli" {
|
rustPackage "dioxus-cli" {
|
||||||
binary = "dx";
|
binary = "dx";
|
||||||
features = [ "no-downloads" ];
|
features = [ "no-downloads" ];
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
packages.default = self'.packages.dioxus-cli;
|
||||||
|
checks.dioxus-cli = self'.packages.dioxus-cli;
|
||||||
|
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
name = "dioxus-dev";
|
name = "dioxus-dev";
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ Blitz combines a number of exciting projects to bring customizable HTML renderin
|
|||||||
Blitz is an extremely capable renderer, often producing results indistinguishable from browsers like Chrome and Safari:
|
Blitz is an extremely capable renderer, often producing results indistinguishable from browsers like Chrome and Safari:
|
||||||
|
|
||||||
|
|
||||||
Not every CSS feature is supported yet, with some bugs like incorrect writing direction or the occasional layout quirk. Our support matrix is here: https://blitz-website.fly.dev/status/css
|
Not every CSS feature is supported yet, with some bugs like incorrect writing direction or the occasional layout quirk.
|
||||||
|
|
||||||
The samples that Blitz can create are quite incredible. Servo’s website:
|
The samples that Blitz can create are quite incredible. Servo’s website:
|
||||||
|
|
||||||
|
|||||||
@@ -145,15 +145,15 @@ pub fn serve_asset(path: &str) -> Result<Response<Vec<u8>>, AssetServeError> {
|
|||||||
/// - [x] iOS
|
/// - [x] iOS
|
||||||
/// - [x] Windows
|
/// - [x] Windows
|
||||||
/// - [x] Linux (appimage)
|
/// - [x] Linux (appimage)
|
||||||
/// - [ ] Linux (rpm)
|
/// - [x] Linux (rpm)
|
||||||
/// - [x] Linux (deb)
|
/// - [x] Linux (deb)
|
||||||
/// - [ ] Android
|
/// - [x] Android (returns current exe.)
|
||||||
#[allow(unreachable_code)]
|
#[allow(unreachable_code)]
|
||||||
fn get_asset_root() -> PathBuf {
|
fn get_asset_root() -> PathBuf {
|
||||||
let cur_exe = std::env::current_exe().unwrap();
|
let cur_exe = std::env::current_exe().unwrap();
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
// In macOS bundles, the assets are in the `Resources` directory
|
||||||
{
|
if cfg!(target_os = "macos") {
|
||||||
return cur_exe
|
return cur_exe
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -162,20 +162,19 @@ fn get_asset_root() -> PathBuf {
|
|||||||
.join("Resources");
|
.join("Resources");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
// In linux bundles, the assets are placed in the lib/$product_name directory
|
||||||
{
|
// bin/
|
||||||
// In linux bundles, the assets are placed in the lib/$product_name directory
|
// main
|
||||||
// bin/
|
// lib/
|
||||||
// main
|
// $product_name/
|
||||||
// lib/
|
// assets/
|
||||||
// $product_name/
|
if cfg!(target_os = "linux") {
|
||||||
// assets/
|
|
||||||
if let Some(product_name) = dioxus_cli_config::product_name() {
|
if let Some(product_name) = dioxus_cli_config::product_name() {
|
||||||
let lib_asset_path = || {
|
let path = cur_exe
|
||||||
let path = cur_exe.parent()?.parent()?.join("lib").join(product_name);
|
.parent()
|
||||||
path.exists().then_some(path)
|
.and_then(|parent| parent.parent())
|
||||||
};
|
.map(|root| root.join("lib").join(product_name));
|
||||||
if let Some(asset_dir) = lib_asset_path() {
|
if let Some(asset_dir) = path.filter(|path| path.exists()) {
|
||||||
return asset_dir;
|
return asset_dir;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/"
|
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
keywords = ["mobile", "gui", "cli", "dioxus", "wasm"]
|
keywords = ["mobile", "gui", "cli", "dioxus", "wasm"]
|
||||||
rust-version = "1.81.0"
|
rust-version = "1.82.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-autofmt = { workspace = true }
|
dioxus-autofmt = { workspace = true }
|
||||||
dioxus-check = { workspace = true }
|
|
||||||
dioxus-rsx-rosetta = { workspace = true }
|
dioxus-rsx-rosetta = { workspace = true }
|
||||||
dioxus-rsx = { workspace = true }
|
dioxus-rsx = { workspace = true }
|
||||||
dioxus-rsx-hotreload = { workspace = true }
|
dioxus-rsx-hotreload = { workspace = true }
|
||||||
@@ -20,8 +19,6 @@ dioxus-core = { workspace = true, features = ["serialize"] }
|
|||||||
dioxus-core-types = { workspace = true }
|
dioxus-core-types = { workspace = true }
|
||||||
dioxus-devtools-types = { workspace = true }
|
dioxus-devtools-types = { workspace = true }
|
||||||
dioxus-cli-config = { workspace = true }
|
dioxus-cli-config = { workspace = true }
|
||||||
dioxus-cli-opt = { workspace = true }
|
|
||||||
dioxus-fullstack = { workspace = true }
|
|
||||||
dioxus-dx-wire-format = { workspace = true }
|
dioxus-dx-wire-format = { workspace = true }
|
||||||
wasm-split-cli = { workspace = true }
|
wasm-split-cli = { workspace = true }
|
||||||
depinfo = { workspace = true }
|
depinfo = { workspace = true }
|
||||||
@@ -33,10 +30,8 @@ bytes = { workspace = true }
|
|||||||
clap = { workspace = true, features = ["derive", "cargo"] }
|
clap = { workspace = true, features = ["derive", "cargo"] }
|
||||||
convert_case = { workspace = true }
|
convert_case = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
uuid = { workspace = true, features = ["v4"] }
|
uuid = { workspace = true, features = ["v4", "v5", "v7"] }
|
||||||
# swc uses serde internals that break with newer versions of serde. We should move off of swc eventually,
|
serde = { workspace = true, features = ["derive"] }
|
||||||
# but pinning serde fixes the issue in the meantime
|
|
||||||
serde = { version = "=1.0.221", features = ["derive"] }
|
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
cargo_toml = { workspace = true, features = ["features"] }
|
cargo_toml = { workspace = true, features = ["features"] }
|
||||||
@@ -61,9 +56,10 @@ console = "0.16.0"
|
|||||||
ctrlc = "3.4.7"
|
ctrlc = "3.4.7"
|
||||||
|
|
||||||
axum = { workspace = true, default-features = true, features = ["ws"] }
|
axum = { workspace = true, default-features = true, features = ["ws"] }
|
||||||
axum-server = { workspace = true, features = ["tls-rustls"] }
|
axum-server = { workspace = true, features = ["tls-rustls-no-provider"] }
|
||||||
axum-extra = { workspace = true, features = ["typed-header"] }
|
axum-extra = { workspace = true, features = ["typed-header"] }
|
||||||
tower-http = { workspace = true, features = ["full"] }
|
tower-http = { workspace = true, features = ["full"] }
|
||||||
|
owo-colors = { workspace = true, features = ["supports-colors"] }
|
||||||
proc-macro2 = { workspace = true, features = ["span-locations"] }
|
proc-macro2 = { workspace = true, features = ["span-locations"] }
|
||||||
syn = { workspace = true, features = [
|
syn = { workspace = true, features = [
|
||||||
"full",
|
"full",
|
||||||
@@ -72,13 +68,12 @@ syn = { workspace = true, features = [
|
|||||||
"visit-mut",
|
"visit-mut",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
headers = "0.4.1"
|
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
dunce = { workspace = true }
|
dunce = { workspace = true }
|
||||||
|
|
||||||
# tools download
|
# tools download
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
reqwest = { workspace = true, features = ["rustls-tls", "trust-dns", "json"] }
|
reqwest = { workspace = true, default-features = false, features = ["rustls-tls", "json"] }
|
||||||
tower = { workspace = true }
|
tower = { workspace = true }
|
||||||
git2 = "0.20.2"
|
git2 = "0.20.2"
|
||||||
|
|
||||||
@@ -95,9 +90,14 @@ toml_edit = "0.22.27"
|
|||||||
prettyplease = { workspace = true }
|
prettyplease = { workspace = true }
|
||||||
|
|
||||||
# Assets
|
# Assets
|
||||||
|
lightningcss = { version = "1.0.0-alpha.67", features = ["browserslist", "into_owned"] }
|
||||||
|
grass = "0.13.4"
|
||||||
|
codemap = "0.1.3"
|
||||||
|
mozjpeg = { version = "0.10.13", default-features = false, features = ["parallel"] }
|
||||||
|
imagequant = "4.4.0"
|
||||||
|
png = "0.17.16"
|
||||||
brotli = "8.0.1"
|
brotli = "8.0.1"
|
||||||
ignore = "0.4.23"
|
ignore = "0.4.23"
|
||||||
env_logger = { workspace = true }
|
|
||||||
const-serialize = { workspace = true, features = ["serde"] }
|
const-serialize = { workspace = true, features = ["serde"] }
|
||||||
const-serialize-07 = { package = "const-serialize", version = "=0.7.2", features = ["serde"] }
|
const-serialize-07 = { package = "const-serialize", version = "=0.7.2", features = ["serde"] }
|
||||||
|
|
||||||
@@ -127,7 +127,6 @@ manganis = { workspace = true }
|
|||||||
manganis-core = { workspace = true }
|
manganis-core = { workspace = true }
|
||||||
manganis-core-07 = { workspace = true }
|
manganis-core-07 = { workspace = true }
|
||||||
target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] }
|
target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] }
|
||||||
wasm-encoder = "0.235.0"
|
|
||||||
|
|
||||||
# Extracting data from an executable
|
# Extracting data from an executable
|
||||||
object = { workspace = true, features = ["all"] }
|
object = { workspace = true, features = ["all"] }
|
||||||
@@ -136,26 +135,26 @@ itertools = { workspace = true }
|
|||||||
throbber-widgets-tui = "0.8.0"
|
throbber-widgets-tui = "0.8.0"
|
||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12.0"
|
||||||
handlebars = "6.3.2"
|
handlebars = "6.3.2"
|
||||||
strum = { version = "0.27.1", features = ["derive"] }
|
|
||||||
memmap = "0.7.0"
|
|
||||||
walrus = { workspace = true, features = ["parallel"] }
|
walrus = { workspace = true, features = ["parallel"] }
|
||||||
wasmparser = { workspace = true }
|
wasmparser = { workspace = true }
|
||||||
|
|
||||||
tauri-utils = { workspace = true }
|
# bundler dependencies
|
||||||
tauri-macos-sign = { workspace = true }
|
icns = { workspace = true }
|
||||||
tauri-bundler = { workspace = true }
|
image = { workspace = true }
|
||||||
|
zip = { workspace = true }
|
||||||
|
sha1 = { workspace = true }
|
||||||
|
sha2 = { workspace = true }
|
||||||
|
md5 = { workspace = true }
|
||||||
|
rpm = { workspace = true }
|
||||||
include_dir = "0.7.4"
|
include_dir = "0.7.4"
|
||||||
flate2 = "1.1.2"
|
flate2 = "1.1.2"
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
local-ip-address = "0.6.5"
|
|
||||||
dircpy = "0.3.19"
|
dircpy = "0.3.19"
|
||||||
plist = "1.7.4"
|
plist = "1.7.4"
|
||||||
memoize = "0.5.1"
|
|
||||||
backtrace = "0.3.74"
|
backtrace = "0.3.74"
|
||||||
ar = "0.9.0"
|
ar = "0.9.0"
|
||||||
wasm-bindgen-externref-xform = "0.2.100"
|
|
||||||
pdb = "0.8.0"
|
pdb = "0.8.0"
|
||||||
self_update = { version = "0.42.0", features = [
|
self_update = { version = "0.42.0", default-features = false, features = [
|
||||||
"archive-tar",
|
"archive-tar",
|
||||||
"archive-zip",
|
"archive-zip",
|
||||||
"compression-flate2",
|
"compression-flate2",
|
||||||
@@ -163,7 +162,6 @@ self_update = { version = "0.42.0", features = [
|
|||||||
] }
|
] }
|
||||||
self-replace = "1.5.0"
|
self-replace = "1.5.0"
|
||||||
cargo-config2 = { workspace = true }
|
cargo-config2 = { workspace = true }
|
||||||
posthog-rs = "0.3.5"
|
|
||||||
serde_json5 = "0.2.1"
|
serde_json5 = "0.2.1"
|
||||||
fs2 = "0.4.3"
|
fs2 = "0.4.3"
|
||||||
sentry-backtrace = "0.42.0"
|
sentry-backtrace = "0.42.0"
|
||||||
@@ -175,9 +173,6 @@ home = "=0.5.11"
|
|||||||
[target.'cfg(target_env = "musl")'.dependencies]
|
[target.'cfg(target_env = "musl")'.dependencies]
|
||||||
git2 = { version = "0.20.2", features = ["vendored-libgit2"] }
|
git2 = { version = "0.20.2", features = ["vendored-libgit2"] }
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
built = { version = "0.8.0", features = ["git2"] }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
tokio-console = ["dep:console-subscriber", "tokio/tracing"]
|
tokio-console = ["dep:console-subscriber", "tokio/tracing"]
|
||||||
@@ -190,6 +185,8 @@ name = "dx"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
escargot = "0.5"
|
escargot = "0.5"
|
||||||
|
indoc = "2.0.6"
|
||||||
|
pretty_assertions = { workspace = true }
|
||||||
|
|
||||||
[package.metadata.binstall]
|
[package.metadata.binstall]
|
||||||
pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }{ archive-suffix }"
|
pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }{ archive-suffix }"
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
+40
-1
@@ -1,3 +1,42 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
built::write_built_file().expect("Failed to acquire build-time information");
|
println!("cargo:rerun-if-env-changed=DIOXUS_CLI_GIT_SHA");
|
||||||
|
println!("cargo:rerun-if-env-changed=DIOXUS_CLI_GIT_SHA_SHORT");
|
||||||
|
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||||
|
println!("cargo:rerun-if-changed=.git/refs");
|
||||||
|
|
||||||
|
let full_hash = std::env::var("DIOXUS_CLI_GIT_SHA")
|
||||||
|
.ok()
|
||||||
|
.or_else(resolve_git_commit_hash);
|
||||||
|
|
||||||
|
if let Some(full_hash) = full_hash {
|
||||||
|
println!("cargo:rustc-env=DIOXUS_CLI_GIT_SHA={full_hash}");
|
||||||
|
|
||||||
|
let short_hash = std::env::var("DIOXUS_CLI_GIT_SHA_SHORT")
|
||||||
|
.ok()
|
||||||
|
.unwrap_or_else(|| full_hash.chars().take(7).collect());
|
||||||
|
|
||||||
|
println!("cargo:rustc-env=DIOXUS_CLI_GIT_SHA_SHORT={short_hash}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_git_commit_hash() -> Option<String> {
|
||||||
|
let output = Command::new("git")
|
||||||
|
.args(["rev-parse", "HEAD"])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hash = String::from_utf8(output.stdout).ok()?;
|
||||||
|
let hash = hash.trim().to_string();
|
||||||
|
|
||||||
|
if hash.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(hash)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3266,14 +3266,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"upgrade_code": {
|
"upgrade_code": {
|
||||||
"description": "A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**, otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app.\n\nBy default, tauri generates this code by generating a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace. You can use Tauri's CLI to generate and print this code for you by running `tauri inspect wix-upgrade-code`.\n\nIt is recommended that you set this value in your tauri config file to avoid accidental changes in your upgrade code whenever you want to change your product name.",
|
"description": "A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**, otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app.\n\nBy default, this code is generated as a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace.\n\nIt is recommended that you set this value in your Dioxus.toml to avoid accidental changes in your upgrade code whenever you want to change your product name.",
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"description": "MSI installer version in the format `major.minor.patch.build` (build is optional).\n\nBecause a valid version is required for MSI installer, it will be derived from [`tauri_bundler::PackageSettings::version`] if this field is not set.\n\nThe first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255. The third and fourth fields have a maximum value of 65,535.\n\nSee <https://learn.microsoft.com/en-us/windows/win32/msi/productversion> for more info.",
|
"description": "MSI installer version in the format `major.minor.patch.build` (build is optional).\n\nBecause a valid version is required for MSI installer, it will be derived from the package version if this field is not set.\n\nThe first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255. The third and fourth fields have a maximum value of 65,535.\n\nSee <https://learn.microsoft.com/en-us/windows/win32/msi/productversion> for more info.",
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
"null"
|
"null"
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::opt::AssetManifest;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use const_serialize::{deserialize_const, serialize_const, ConstVec};
|
use const_serialize::{deserialize_const, serialize_const, ConstVec};
|
||||||
use dioxus_cli_opt::AssetManifest;
|
|
||||||
use manganis::{AssetOptions, AssetVariant, BundledAsset, ImageFormat, ImageSize};
|
use manganis::{AssetOptions, AssetVariant, BundledAsset, ImageFormat, ImageSize};
|
||||||
use manganis_core::{AndroidArtifactMetadata, SwiftPackageMetadata, SymbolData};
|
use manganis_core::{AndroidArtifactMetadata, SwiftPackageMetadata, SymbolData};
|
||||||
use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol};
|
use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol};
|
||||||
@@ -811,7 +811,7 @@ pub(crate) async fn extract_symbols_from_file(
|
|||||||
// Add the hash to each asset in parallel
|
// Add the hash to each asset in parallel
|
||||||
assets
|
assets
|
||||||
.par_iter_mut()
|
.par_iter_mut()
|
||||||
.for_each(dioxus_cli_opt::add_hash_to_asset);
|
.for_each(crate::opt::add_hash_to_asset);
|
||||||
|
|
||||||
// Write back only assets to the binary file (permissions are not modified)
|
// Write back only assets to the binary file (permissions are not modified)
|
||||||
for entry in write_entries {
|
for entry in write_entries {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
use crate::opt::process_file_to;
|
||||||
use crate::{
|
use crate::{
|
||||||
build::cache::ObjectCache, serve::WebServer, verbosity_or_default, BuildArtifacts,
|
build::cache::ObjectCache, serve::WebServer, verbosity_or_default, BuildArtifacts,
|
||||||
BuildRequest, BuildStage, BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result,
|
BuildRequest, BuildStage, BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result,
|
||||||
RustcArgs, StructuredOutput,
|
RustcArgs, StructuredOutput,
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Context, Error};
|
use anyhow::{bail, Context, Error};
|
||||||
use dioxus_cli_opt::process_file_to;
|
|
||||||
use futures_util::{future::OptionFuture, pin_mut, FutureExt};
|
use futures_util::{future::OptionFuture, pin_mut, FutureExt};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -475,9 +475,12 @@ impl AppBuilder {
|
|||||||
current,
|
current,
|
||||||
total,
|
total,
|
||||||
krate,
|
krate,
|
||||||
|
fresh,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
tracing::info!("Compiled [{current:>3}/{total}]: {krate}");
|
if !fresh {
|
||||||
|
tracing::info!("Compiled [{current:>3}/{total}]: {krate}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BuildStage::RunningBindgen => tracing::info!("Running wasm-bindgen..."),
|
BuildStage::RunningBindgen => tracing::info!("Running wasm-bindgen..."),
|
||||||
BuildStage::CopyingAssets {
|
BuildStage::CopyingAssets {
|
||||||
@@ -509,7 +512,7 @@ impl AppBuilder {
|
|||||||
}
|
}
|
||||||
BuilderUpdate::BuildFailed { err } => {
|
BuilderUpdate::BuildFailed { err } => {
|
||||||
// Flush remaining compiler messages
|
// Flush remaining compiler messages
|
||||||
while let Ok(Some(msg)) = self.rx.try_next() {
|
while let Ok(msg) = self.rx.try_recv() {
|
||||||
if let BuilderUpdate::CompilerMessage { message } = msg {
|
if let BuilderUpdate::CompilerMessage { message } = msg {
|
||||||
tracing::info!(json = %StructuredOutput::RustcOutput { message: message.clone() }, %message);
|
tracing::info!(json = %StructuredOutput::RustcOutput { message: message.clone() }, %message);
|
||||||
}
|
}
|
||||||
@@ -760,7 +763,8 @@ impl AppBuilder {
|
|||||||
let to = asset_dir.join(bundled.bundled_path());
|
let to = asset_dir.join(bundled.bundled_path());
|
||||||
|
|
||||||
tracing::debug!("Copying asset from patch: {}", from.display());
|
tracing::debug!("Copying asset from patch: {}", from.display());
|
||||||
if let Err(e) = dioxus_cli_opt::process_file_to(bundled.options(), &from, &to) {
|
let esbuild = crate::esbuild::Esbuild::path_if_installed();
|
||||||
|
if let Err(e) = process_file_to(bundled.options(), &from, &to, esbuild.as_deref()) {
|
||||||
tracing::error!("Failed to copy asset: {e}");
|
tracing::error!("Failed to copy asset: {e}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -869,7 +873,8 @@ impl AppBuilder {
|
|||||||
// the asset would be in a new location because the contents and hash have changed. Since we are
|
// the asset would be in a new location because the contents and hash have changed. Since we are
|
||||||
// hotreloading, we need to use the old asset location it was originally written to.
|
// hotreloading, we need to use the old asset location it was originally written to.
|
||||||
let options = *resource.options();
|
let options = *resource.options();
|
||||||
let res = process_file_to(&options, &changed_file, &output_path);
|
let esbuild = crate::esbuild::Esbuild::path_if_installed();
|
||||||
|
let res = process_file_to(&options, &changed_file, &output_path, esbuild.as_deref());
|
||||||
let bundled_name = PathBuf::from(resource.bundled_path());
|
let bundled_name = PathBuf::from(resource.bundled_path());
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
tracing::debug!("Failed to hotreload asset {e}");
|
tracing::debug!("Failed to hotreload asset {e}");
|
||||||
|
|||||||
@@ -137,12 +137,19 @@ impl BuildContext {
|
|||||||
tracing::trace!(dx_src = ?TraceSrc::Cargo, "{line}");
|
tracing::trace!(dx_src = ?TraceSrc::Cargo, "{line}");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn status_build_progress(&self, count: usize, total: usize, name: String) {
|
pub(crate) fn status_build_progress(
|
||||||
|
&self,
|
||||||
|
count: usize,
|
||||||
|
total: usize,
|
||||||
|
name: String,
|
||||||
|
fresh: bool,
|
||||||
|
) {
|
||||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||||
stage: BuildStage::Compiling {
|
stage: BuildStage::Compiling {
|
||||||
current: count,
|
current: count,
|
||||||
total,
|
total,
|
||||||
krate: name,
|
krate: name,
|
||||||
|
fresh,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,9 +144,9 @@ pub async fn create_framework_bundle(
|
|||||||
std::fs::write(resources_dir.join("Info.plist"), info_plist)?;
|
std::fs::write(resources_dir.join("Info.plist"), info_plist)?;
|
||||||
|
|
||||||
// Create symbolic links (required for macOS framework structure)
|
// Create symbolic links (required for macOS framework structure)
|
||||||
let versions_dir = framework_dir.join("Versions");
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
|
let versions_dir = framework_dir.join("Versions");
|
||||||
std::os::unix::fs::symlink("A", versions_dir.join("Current"))?;
|
std::os::unix::fs::symlink("A", versions_dir.join("Current"))?;
|
||||||
std::os::unix::fs::symlink(
|
std::os::unix::fs::symlink(
|
||||||
format!("Versions/Current/{}", framework_name),
|
format!("Versions/Current/{}", framework_name),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
//! Eventually, we might want to expand this to include more metadata about the build, including
|
//! Eventually, we might want to expand this to include more metadata about the build, including
|
||||||
//! build time, target platform, etc.
|
//! build time, target platform, etc.
|
||||||
|
|
||||||
use dioxus_cli_opt::AssetManifest;
|
use crate::opt::AssetManifest;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize)]
|
#[derive(Default, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -1420,8 +1420,25 @@ fn name_is_bindgen_symbol(name: &str) -> bool {
|
|||||||
|| name.contains("__wbindgen_externref")
|
|| name.contains("__wbindgen_externref")
|
||||||
|| name.contains("wasm_bindgen8describe6inform")
|
|| name.contains("wasm_bindgen8describe6inform")
|
||||||
|| name.contains("wasm_bindgen..describe..WasmDescribe")
|
|| name.contains("wasm_bindgen..describe..WasmDescribe")
|
||||||
|| name.contains("wasm_bindgen..closure..WasmClosure$GT$8describe")
|
|| (name.contains("wasm_bindgen..closure..WasmClosure") && name.contains("describe"))
|
||||||
|| name.contains("wasm_bindgen7closure16Closure$LT$T$GT$4wrap8describe")
|
|| (name.contains("wasm_bindgen7closure16Closure") && name.contains("describe"))
|
||||||
|
|| (name.contains("wasm_bindgen7convert8closures") && name.contains("describe_invoke"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for bindgen symbols. As we find more bad symbols, add them here
|
||||||
|
#[test]
|
||||||
|
fn bindgen_symbol_catch() {
|
||||||
|
let symbol = "_ZN12wasm_bindgen7convert8closures1_142_$LT$impl$u20$wasm_bindgen..closure..WasmClosure$u20$for$u20$dyn$u20$core..ops..function..FnMut$LT$$LP$$RP$$GT$$u2b$Output$u20$$u3d$$u20$R$GT$15describe_invoke17h4373f8b6570333dcE";
|
||||||
|
assert!(name_is_bindgen_symbol(symbol));
|
||||||
|
|
||||||
|
// matches_legacy_wasm_bindgen_closure_describe_symbols
|
||||||
|
let symbol = "_ZN12wasm_bindgen7closure16Closure$LT$T$GT$4wrap8describe17h1234567890abcdefE";
|
||||||
|
assert!(name_is_bindgen_symbol(symbol));
|
||||||
|
|
||||||
|
// does_not_match_saved_runtime_exports
|
||||||
|
assert!(!name_is_bindgen_symbol("__wbindgen_malloc"));
|
||||||
|
assert!(!name_is_bindgen_symbol("__wbindgen_realloc"));
|
||||||
|
assert!(!name_is_bindgen_symbol("__wbindgen_free"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manually parse the data section from a wasm module
|
/// Manually parse the data section from a wasm module
|
||||||
|
|||||||
@@ -320,6 +320,7 @@
|
|||||||
//! - xbuild: <https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs>
|
//! - xbuild: <https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs>
|
||||||
|
|
||||||
use super::HotpatchModuleCache;
|
use super::HotpatchModuleCache;
|
||||||
|
use crate::opt::{process_file_to, AssetManifest};
|
||||||
use crate::{
|
use crate::{
|
||||||
AndroidTools, AppManifest, BuildContext, BuildId, BundleFormat, DioxusConfig, Error,
|
AndroidTools, AppManifest, BuildContext, BuildId, BundleFormat, DioxusConfig, Error,
|
||||||
LinkAction, LinkerFlavor, ObjectCache, Platform, Renderer, Result, RustcArgs, TargetArgs,
|
LinkAction, LinkerFlavor, ObjectCache, Platform, Renderer, Result, RustcArgs, TargetArgs,
|
||||||
@@ -331,7 +332,6 @@ use cargo_toml::{Profile, Profiles, StripSetting};
|
|||||||
use depinfo::RustcDepInfo;
|
use depinfo::RustcDepInfo;
|
||||||
use dioxus_cli_config::{format_base_path_meta_element, PRODUCT_NAME_ENV};
|
use dioxus_cli_config::{format_base_path_meta_element, PRODUCT_NAME_ENV};
|
||||||
use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV};
|
use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV};
|
||||||
use dioxus_cli_opt::{process_file_to, AssetManifest};
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use krates::{cm::TargetKind, NodeId};
|
use krates::{cm::TargetKind, NodeId};
|
||||||
use manganis::{AssetOptions, BundledAsset, SwiftPackageMetadata};
|
use manganis::{AssetOptions, BundledAsset, SwiftPackageMetadata};
|
||||||
@@ -1306,7 +1306,12 @@ impl BuildRequest {
|
|||||||
}
|
}
|
||||||
Message::CompilerArtifact(artifact) => {
|
Message::CompilerArtifact(artifact) => {
|
||||||
units_compiled += 1;
|
units_compiled += 1;
|
||||||
ctx.status_build_progress(units_compiled, crate_count, artifact.target.name);
|
ctx.status_build_progress(
|
||||||
|
units_compiled,
|
||||||
|
crate_count,
|
||||||
|
artifact.target.name,
|
||||||
|
artifact.fresh,
|
||||||
|
);
|
||||||
output_location = artifact.executable.map(Into::into);
|
output_location = artifact.executable.map(Into::into);
|
||||||
}
|
}
|
||||||
// todo: this can occasionally swallow errors, so we should figure out what exactly is going wrong
|
// todo: this can occasionally swallow errors, so we should figure out what exactly is going wrong
|
||||||
@@ -2280,6 +2285,7 @@ impl BuildRequest {
|
|||||||
// Parallel Copy over the assets and keep track of progress with an atomic counter
|
// Parallel Copy over the assets and keep track of progress with an atomic counter
|
||||||
let progress = ctx.tx.clone();
|
let progress = ctx.tx.clone();
|
||||||
let ws_dir = self.workspace_dir();
|
let ws_dir = self.workspace_dir();
|
||||||
|
let esbuild_path = crate::esbuild::Esbuild::path_if_installed();
|
||||||
|
|
||||||
// Optimizing assets is expensive and blocking, so we do it in a tokio spawn blocking task
|
// Optimizing assets is expensive and blocking, so we do it in a tokio spawn blocking task
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
@@ -2292,7 +2298,7 @@ impl BuildRequest {
|
|||||||
"Starting asset copy {processing}/{asset_count} from {from_:?}"
|
"Starting asset copy {processing}/{asset_count} from {from_:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
let res = process_file_to(options, from, to);
|
let res = process_file_to(options, from, to, esbuild_path.as_deref());
|
||||||
if let Err(err) = res.as_ref() {
|
if let Err(err) = res.as_ref() {
|
||||||
tracing::error!("Failed to copy asset {from:?}: {err}");
|
tracing::error!("Failed to copy asset {from:?}: {err}");
|
||||||
}
|
}
|
||||||
@@ -5524,6 +5530,25 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
|||||||
.join("app-debug.apk")
|
.join("app-debug.apk")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn release_apk_path(&self) -> PathBuf {
|
||||||
|
self.root_dir()
|
||||||
|
.join("app")
|
||||||
|
.join("build")
|
||||||
|
.join("outputs")
|
||||||
|
.join("apk")
|
||||||
|
.join("release")
|
||||||
|
.join("app-release.apk")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn android_apk_path(&self) -> PathBuf {
|
||||||
|
let assembled_release = self.release && self.config.bundle.android.is_some();
|
||||||
|
if assembled_release {
|
||||||
|
self.release_apk_path()
|
||||||
|
} else {
|
||||||
|
self.debug_apk_path()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// We only really currently care about:
|
/// We only really currently care about:
|
||||||
///
|
///
|
||||||
/// - app dir (.app, .exe, .apk, etc)
|
/// - app dir (.app, .exe, .apk, etc)
|
||||||
@@ -5683,6 +5708,9 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
|||||||
|
|
||||||
self.verify_toolchain_installed().await?;
|
self.verify_toolchain_installed().await?;
|
||||||
|
|
||||||
|
// esbuild is used for JS asset processing on all platforms
|
||||||
|
let _esbuild_path = crate::esbuild::Esbuild::get_or_install().await?;
|
||||||
|
|
||||||
match self.bundle {
|
match self.bundle {
|
||||||
BundleFormat::Web => self.verify_web_tooling().await?,
|
BundleFormat::Web => self.verify_web_tooling().await?,
|
||||||
BundleFormat::Ios => self.verify_ios_tooling().await?,
|
BundleFormat::Ios => self.verify_ios_tooling().await?,
|
||||||
@@ -5970,7 +5998,7 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
|||||||
if let Some(name) = entry.file_name().to_str() {
|
if let Some(name) = entry.file_name().to_str() {
|
||||||
if name.starts_with(&prefix) && name.ends_with(".rlib") {
|
if name.starts_with(&prefix) && name.ends_with(".rlib") {
|
||||||
let mtime = entry.metadata().ok()?.modified().ok()?;
|
let mtime = entry.metadata().ok()?.modified().ok()?;
|
||||||
if best.as_ref().map_or(true, |(_, t)| mtime > *t) {
|
if best.as_ref().is_none_or(|(_, t)| mtime > *t) {
|
||||||
best = Some((entry.path(), mtime));
|
best = Some((entry.path(), mtime));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
/// Merge the new profiles into the target profiles. Keep the existing values if they exist.
|
||||||
fn merge_profiles(target: &mut Profiles, new: Profiles) {
|
fn merge_profiles(target: &mut Profiles, new: Profiles) {
|
||||||
if let Some(new_release) = new.release {
|
if let Some(new_release) = new.release {
|
||||||
if target.release.is_none() {
|
if let Some(profile) = target.release.as_mut() {
|
||||||
target.release = Some(new_release);
|
merge_profile(profile, new_release);
|
||||||
} else {
|
} else {
|
||||||
merge_profile(target.release.as_mut().unwrap(), new_release);
|
target.release = Some(new_release);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(new_dev) = new.dev {
|
if let Some(new_dev) = new.dev {
|
||||||
if target.dev.is_none() {
|
if let Some(profile) = target.dev.as_mut() {
|
||||||
target.dev = Some(new_dev);
|
merge_profile(profile, new_dev);
|
||||||
} else {
|
} else {
|
||||||
merge_profile(target.dev.as_mut().unwrap(), new_dev);
|
target.dev = Some(new_dev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(new_test) = new.test {
|
if let Some(new_test) = new.test {
|
||||||
if target.test.is_none() {
|
if let Some(profile) = target.test.as_mut() {
|
||||||
target.test = Some(new_test);
|
merge_profile(profile, new_test);
|
||||||
} else {
|
} else {
|
||||||
merge_profile(target.test.as_mut().unwrap(), new_test);
|
target.test = Some(new_test);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(new_bench) = new.bench {
|
if let Some(new_bench) = new.bench {
|
||||||
if target.bench.is_none() {
|
if let Some(profile) = target.bench.as_mut() {
|
||||||
target.bench = Some(new_bench);
|
merge_profile(profile, new_bench);
|
||||||
} else {
|
} else {
|
||||||
merge_profile(target.bench.as_mut().unwrap(), new_bench);
|
target.bench = Some(new_bench);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
if let Some(new_doc) = new.doc {
|
if let Some(new_doc) = new.doc {
|
||||||
if target.doc.is_none() {
|
if let Some(profile) = target.doc.as_mut() {
|
||||||
target.doc = Some(new_doc);
|
merge_profile(profile, new_doc);
|
||||||
} else {
|
} else {
|
||||||
merge_profile(target.doc.as_mut().unwrap(), new_doc);
|
target.doc = Some(new_doc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::metadata::{
|
use super::metadata::{
|
||||||
AnyLoopInfo, AsyncInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, MatchInfo,
|
AnyLoopInfo, AsyncInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, MatchInfo,
|
||||||
WhileInfo,
|
WhileInfo,
|
||||||
};
|
};
|
||||||
@@ -229,7 +229,7 @@ impl std::fmt::Display for Issue {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::check_file;
|
use crate::check::check_file;
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
@@ -168,6 +168,7 @@ pub struct Span {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Span {
|
impl Span {
|
||||||
|
#[cfg(test)]
|
||||||
pub fn new_from_str(source_text: &str, start: LineColumn) -> Self {
|
pub fn new_from_str(source_text: &str, start: LineColumn) -> Self {
|
||||||
let mut lines = source_text.lines();
|
let mut lines = source_text.lines();
|
||||||
let first_line = lines.next().unwrap_or_default();
|
let first_line = lines.next().unwrap_or_default();
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
use std::path::PathBuf;
|
mod issues;
|
||||||
|
mod metadata;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
use syn::{spanned::Spanned, visit::Visit, Pat};
|
use syn::{spanned::Spanned, visit::Visit, Pat};
|
||||||
|
|
||||||
use crate::{
|
use {
|
||||||
issues::{Issue, IssueReport},
|
issues::{Issue, IssueReport},
|
||||||
metadata::{
|
metadata::{
|
||||||
AnyLoopInfo, AsyncInfo, ClosureInfo, ComponentInfo, ConditionalInfo, FnInfo, ForInfo,
|
AnyLoopInfo, AsyncInfo, ClosureInfo, ComponentInfo, ConditionalInfo, FnInfo, ForInfo,
|
||||||
@@ -289,7 +291,7 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::metadata::{
|
use crate::check::metadata::{
|
||||||
AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, LineColumn, LoopInfo,
|
AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, LineColumn, LoopInfo,
|
||||||
MatchInfo, Span, WhileInfo,
|
MatchInfo, Span, WhileInfo,
|
||||||
};
|
};
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::{fs::create_dir_all, path::PathBuf};
|
use std::{fs::create_dir_all, path::PathBuf};
|
||||||
|
|
||||||
|
use crate::opt::process_file_to;
|
||||||
use crate::{extract_assets_from_file, Result, StructuredOutput};
|
use crate::{extract_assets_from_file, Result, StructuredOutput};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use dioxus_cli_opt::process_file_to;
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Parser)]
|
#[derive(Clone, Debug, Parser)]
|
||||||
@@ -28,7 +28,7 @@ impl BuildAssets {
|
|||||||
destination_path.display(),
|
destination_path.display(),
|
||||||
asset
|
asset
|
||||||
);
|
);
|
||||||
process_file_to(asset.options(), &source_path, &destination_path)?;
|
process_file_to(asset.options(), &source_path, &destination_path, None)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(StructuredOutput::Success)
|
Ok(StructuredOutput::Success)
|
||||||
|
|||||||
+177
-162
@@ -1,10 +1,10 @@
|
|||||||
use crate::{AppBuilder, BuildArgs, BuildId, BuildMode, BuildRequest, BundleFormat};
|
use crate::{
|
||||||
use anyhow::{bail, Context};
|
AppBuilder, BuildArgs, BuildId, BuildMode, BuildRequest, BundleFormat, PackageType, Platform,
|
||||||
|
};
|
||||||
|
use anyhow::Context;
|
||||||
use path_absolutize::Absolutize;
|
use path_absolutize::Absolutize;
|
||||||
use std::collections::HashMap;
|
use std::path::PathBuf;
|
||||||
use tauri_bundler::{BundleBinary, BundleSettings, PackageSettings, SettingsBuilder};
|
use target_lexicon::Triple;
|
||||||
|
|
||||||
use walkdir::WalkDir;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ use super::*;
|
|||||||
pub struct Bundle {
|
pub struct Bundle {
|
||||||
/// The package types to bundle
|
/// The package types to bundle
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub package_types: Option<Vec<crate::PackageType>>,
|
pub package_types: Option<Vec<PackageType>>,
|
||||||
|
|
||||||
/// The directory in which the final bundle will be placed.
|
/// The directory in which the final bundle will be placed.
|
||||||
///
|
///
|
||||||
@@ -37,15 +37,14 @@ impl Bundle {
|
|||||||
pub(crate) async fn bundle(mut self) -> Result<StructuredOutput> {
|
pub(crate) async fn bundle(mut self) -> Result<StructuredOutput> {
|
||||||
tracing::info!("Bundling project...");
|
tracing::info!("Bundling project...");
|
||||||
|
|
||||||
|
self.force_ios_bundle_device_target_if_needed()?;
|
||||||
let BuildTargets { client, server } = self.args.into_targets().await?;
|
let BuildTargets { client, server } = self.args.into_targets().await?;
|
||||||
|
|
||||||
let mut server_artifacts = None;
|
|
||||||
let client_artifacts =
|
let client_artifacts =
|
||||||
AppBuilder::started(&client, BuildMode::Base { run: false }, BuildId::PRIMARY)?
|
AppBuilder::started(&client, BuildMode::Base { run: false }, BuildId::PRIMARY)?
|
||||||
.finish_build()
|
.finish_build()
|
||||||
.await?;
|
.await?;
|
||||||
|
let mut server_artifacts = None;
|
||||||
tracing::info!(path = ?client.root_dir(), "Client build completed successfully! 🚀");
|
|
||||||
|
|
||||||
if let Some(server) = server.as_ref() {
|
if let Some(server) = server.as_ref() {
|
||||||
// If the server is present, we need to build it as well
|
// If the server is present, we need to build it as well
|
||||||
@@ -58,11 +57,13 @@ impl Bundle {
|
|||||||
tracing::info!(path = ?client.root_dir(), "Server build completed successfully! 🚀");
|
tracing::info!(path = ?client.root_dir(), "Server build completed successfully! 🚀");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're building for iOS, we need to bundle the iOS bundle
|
// Fill platform-specific defaults for package types when omitted.
|
||||||
if client.bundle == BundleFormat::Ios && self.package_types.is_none() {
|
if self.package_types.is_none() {
|
||||||
self.package_types = Some(vec![crate::PackageType::IosBundle]);
|
self.package_types = Self::default_package_types(client.bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Self::validate_package_types_for_bundle(client.bundle, self.package_types.as_deref())?;
|
||||||
|
|
||||||
let mut bundles = vec![];
|
let mut bundles = vec![];
|
||||||
|
|
||||||
// Copy the server over if it exists
|
// Copy the server over if it exists
|
||||||
@@ -70,31 +71,24 @@ impl Bundle {
|
|||||||
bundles.push(server.main_exe());
|
bundles.push(server.main_exe());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a list of bundles that we might need to copy
|
// Create a list of bundles that we might need to copy.
|
||||||
|
// Package-type based bundling is handled by the bundler module.
|
||||||
match client.bundle {
|
match client.bundle {
|
||||||
// By default, mac/win/linux work with tauri bundle
|
// Desktop and Android platforms use package-type dispatch in the bundler module.
|
||||||
BundleFormat::MacOS | BundleFormat::Linux | BundleFormat::Windows => {
|
BundleFormat::MacOS
|
||||||
tracing::info!("Running desktop bundler...");
|
| BundleFormat::Ios
|
||||||
for bundle in Self::bundle_desktop(&client, &self.package_types)? {
|
| BundleFormat::Linux
|
||||||
|
| BundleFormat::Windows
|
||||||
|
| BundleFormat::Android => {
|
||||||
|
tracing::info!("Running package bundler...");
|
||||||
|
for bundle in Self::bundle_with_package_types(&client, &self.package_types).await? {
|
||||||
bundles.extend(bundle.bundle_paths);
|
bundles.extend(bundle.bundle_paths);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web/ios can just use their root_dir
|
// Web can use its root_dir directly.
|
||||||
BundleFormat::Web => bundles.push(client.root_dir()),
|
BundleFormat::Web => bundles.push(client.root_dir()),
|
||||||
BundleFormat::Ios => {
|
|
||||||
tracing::warn!("iOS bundles are not currently codesigned! You will need to codesign the app before distributing.");
|
|
||||||
bundles.push(client.root_dir())
|
|
||||||
}
|
|
||||||
BundleFormat::Server => bundles.push(client.root_dir()),
|
BundleFormat::Server => bundles.push(client.root_dir()),
|
||||||
|
|
||||||
BundleFormat::Android => {
|
|
||||||
let aab = client
|
|
||||||
.android_gradle_bundle()
|
|
||||||
.await
|
|
||||||
.context("Failed to run gradle bundleRelease")?;
|
|
||||||
bundles.push(aab);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Copy the bundles to the output directory if one was specified
|
// Copy the bundles to the output directory if one was specified
|
||||||
@@ -148,147 +142,45 @@ impl Bundle {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bundle_desktop(
|
async fn bundle_with_package_types(
|
||||||
build: &BuildRequest,
|
build: &BuildRequest,
|
||||||
package_types: &Option<Vec<crate::PackageType>>,
|
package_types: &Option<Vec<PackageType>>,
|
||||||
) -> Result<Vec<tauri_bundler::Bundle>, Error> {
|
) -> Result<Vec<crate::bundler::Bundle>, Error> {
|
||||||
let krate = &build;
|
use anyhow::bail;
|
||||||
let exe = build.main_exe();
|
|
||||||
|
|
||||||
_ = std::fs::remove_dir_all(krate.bundle_dir(build.bundle));
|
if matches!(
|
||||||
|
build.bundle,
|
||||||
|
BundleFormat::MacOS | BundleFormat::Linux | BundleFormat::Windows
|
||||||
|
) {
|
||||||
|
let krate = &build;
|
||||||
|
let exe = build.main_exe();
|
||||||
|
|
||||||
let package = krate.package();
|
_ = std::fs::remove_dir_all(krate.bundle_dir(build.bundle));
|
||||||
let mut name: PathBuf = krate.executable_name().into();
|
|
||||||
if cfg!(windows) {
|
|
||||||
name.set_extension("exe");
|
|
||||||
}
|
|
||||||
std::fs::create_dir_all(krate.bundle_dir(build.bundle))
|
|
||||||
.context("Failed to create bundle directory")?;
|
|
||||||
std::fs::copy(&exe, krate.bundle_dir(build.bundle).join(&name))
|
|
||||||
.with_context(|| "Failed to copy the output executable into the bundle directory")?;
|
|
||||||
|
|
||||||
let binaries = vec![
|
let mut name: PathBuf = krate.executable_name().into();
|
||||||
// We use the name of the exe but it has to be in the same directory
|
if cfg!(windows) {
|
||||||
BundleBinary::new(krate.executable_name().to_string(), true)
|
name.set_extension("exe");
|
||||||
.set_src_path(Some(exe.display().to_string())),
|
}
|
||||||
];
|
std::fs::create_dir_all(krate.bundle_dir(build.bundle))
|
||||||
|
.context("Failed to create bundle directory")?;
|
||||||
|
std::fs::copy(&exe, krate.bundle_dir(build.bundle).join(&name)).with_context(|| {
|
||||||
|
"Failed to copy the output executable into the bundle directory"
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut bundle_settings: BundleSettings = krate.config.bundle.clone().into();
|
// Check if required fields are provided instead of failing silently.
|
||||||
|
if build.config.bundle.identifier.is_none() {
|
||||||
// Check if required fields are provided instead of failing silently.
|
bail!("\n\nBundle identifier was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\nidentifier = \"com.mycompany\"\n\n");
|
||||||
if bundle_settings.identifier.is_none() {
|
}
|
||||||
bail!("\n\nBundle identifier was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\nidentifier = \"com.mycompany\"\n\n");
|
if build.config.bundle.publisher.is_none() {
|
||||||
}
|
bail!("\n\nBundle publisher was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\npublisher = \"MyCompany\"\n\n");
|
||||||
if bundle_settings.publisher.is_none() {
|
|
||||||
bail!("\n\nBundle publisher was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\npublisher = \"MyCompany\"\n\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve an icon path relative to the crate dir
|
|
||||||
fn canonicalize_icon_path(build: &BuildRequest, icon: &mut String) -> Result<(), Error> {
|
|
||||||
let icon_path = build
|
|
||||||
.crate_dir()
|
|
||||||
.join(&icon)
|
|
||||||
.canonicalize()
|
|
||||||
.with_context(|| format!("Failed to canonicalize path to icon {icon:?}"))?;
|
|
||||||
*icon = icon_path.to_string_lossy().to_string();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve bundle.icon relative to the crate dir
|
|
||||||
if let Some(icons) = bundle_settings.icon.as_mut() {
|
|
||||||
for icon in icons.iter_mut() {
|
|
||||||
canonicalize_icon_path(build, icon)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(deprecated)]
|
let ctx = crate::bundler::BundleContext::new(build, package_types).await?;
|
||||||
if cfg!(windows) {
|
|
||||||
// Resolve bundle.windows.icon_path relative to the crate dir
|
|
||||||
let mut windows_icon_path = bundle_settings
|
|
||||||
.windows
|
|
||||||
.icon_path
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string();
|
|
||||||
canonicalize_icon_path(build, &mut windows_icon_path)?;
|
|
||||||
bundle_settings.windows.icon_path = PathBuf::from(&windows_icon_path);
|
|
||||||
|
|
||||||
let windows_icon_override = krate.config.bundle.windows.as_ref().map(|w| &w.icon_path);
|
tracing::debug!("Bundling project for {:?}", ctx.package_types());
|
||||||
if windows_icon_override.is_none() {
|
|
||||||
let icon_path = bundle_settings
|
|
||||||
.icon
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|icons| icons.first());
|
|
||||||
|
|
||||||
if let Some(icon_path) = icon_path {
|
let bundles = ctx.bundle_project().await.inspect_err(|err| {
|
||||||
bundle_settings.icon = Some(vec![icon_path.into()]);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bundle_settings.resources_map.is_none() {
|
|
||||||
bundle_settings.resources_map = Some(HashMap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let asset_dir = build.asset_dir();
|
|
||||||
if asset_dir.exists() {
|
|
||||||
for entry in WalkDir::new(&asset_dir) {
|
|
||||||
let entry = entry.unwrap();
|
|
||||||
let path = entry.path();
|
|
||||||
|
|
||||||
if path.is_file() {
|
|
||||||
let old = path
|
|
||||||
.canonicalize()
|
|
||||||
.with_context(|| format!("Failed to canonicalize {entry:?}"))?;
|
|
||||||
let new =
|
|
||||||
PathBuf::from("assets").join(path.strip_prefix(&asset_dir).unwrap_or(path));
|
|
||||||
|
|
||||||
tracing::debug!("Bundled asset: {old:?} -> {new:?}");
|
|
||||||
bundle_settings
|
|
||||||
.resources_map
|
|
||||||
.as_mut()
|
|
||||||
.expect("to be set")
|
|
||||||
.insert(old.display().to_string(), new.display().to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for resource_path in bundle_settings.resources.take().into_iter().flatten() {
|
|
||||||
bundle_settings
|
|
||||||
.resources_map
|
|
||||||
.as_mut()
|
|
||||||
.expect("to be set")
|
|
||||||
.insert(resource_path, "".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut settings = SettingsBuilder::new()
|
|
||||||
.project_out_directory(krate.bundle_dir(build.bundle))
|
|
||||||
.package_settings(PackageSettings {
|
|
||||||
product_name: krate.bundled_app_name(),
|
|
||||||
version: package.version.to_string(),
|
|
||||||
description: package.description.clone().unwrap_or_default(),
|
|
||||||
homepage: Some(package.homepage.clone().unwrap_or_default()),
|
|
||||||
authors: Some(package.authors.clone()),
|
|
||||||
default_run: Some(name.display().to_string()),
|
|
||||||
})
|
|
||||||
.log_level(log::Level::Debug)
|
|
||||||
.binaries(binaries)
|
|
||||||
.bundle_settings(bundle_settings);
|
|
||||||
|
|
||||||
if let Some(packages) = &package_types {
|
|
||||||
settings = settings.package_types(packages.iter().map(|p| (*p).into()).collect());
|
|
||||||
}
|
|
||||||
|
|
||||||
settings = settings.target(build.triple.to_string());
|
|
||||||
|
|
||||||
let settings = settings
|
|
||||||
.build()
|
|
||||||
.context("failed to bundle tauri bundle settings")?;
|
|
||||||
tracing::debug!("Bundling project with settings: {:#?}", settings);
|
|
||||||
if cfg!(target_os = "macos") {
|
|
||||||
std::env::set_var("CI", "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
let bundles = tauri_bundler::bundle::bundle_project(&settings).inspect_err(|err| {
|
|
||||||
tracing::error!("Failed to bundle project: {:#?}", err);
|
tracing::error!("Failed to bundle project: {:#?}", err);
|
||||||
if cfg!(target_os = "macos") {
|
if cfg!(target_os = "macos") {
|
||||||
tracing::error!("Make sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)");
|
tracing::error!("Make sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)");
|
||||||
@@ -297,4 +189,127 @@ impl Bundle {
|
|||||||
|
|
||||||
Ok(bundles)
|
Ok(bundles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_package_types(bundle: BundleFormat) -> Option<Vec<PackageType>> {
|
||||||
|
match bundle {
|
||||||
|
BundleFormat::Ios => Some(vec![crate::PackageType::Ipa]),
|
||||||
|
BundleFormat::Android => Some(vec![crate::PackageType::Aab]),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn force_ios_bundle_device_target_if_needed(&mut self) -> Result<()> {
|
||||||
|
let client_args = self
|
||||||
|
.args
|
||||||
|
.client
|
||||||
|
.as_mut()
|
||||||
|
.map(|args| &mut args.build_arguments)
|
||||||
|
.unwrap_or(&mut self.args.shared.build_arguments);
|
||||||
|
|
||||||
|
if client_args.platform == Platform::Ios && client_args.target.is_none() {
|
||||||
|
client_args.target = Some("aarch64-apple-ios".parse::<Triple>()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_package_types_for_bundle(
|
||||||
|
bundle: BundleFormat,
|
||||||
|
package_types: Option<&[PackageType]>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let Some(package_types) = package_types else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_package_supported = |package_type: &PackageType| -> bool {
|
||||||
|
match bundle {
|
||||||
|
BundleFormat::MacOS => matches!(
|
||||||
|
package_type,
|
||||||
|
PackageType::MacOsBundle | PackageType::Dmg | PackageType::Updater
|
||||||
|
),
|
||||||
|
BundleFormat::Linux => matches!(
|
||||||
|
package_type,
|
||||||
|
PackageType::Deb
|
||||||
|
| PackageType::Rpm
|
||||||
|
| PackageType::AppImage
|
||||||
|
| PackageType::Updater
|
||||||
|
),
|
||||||
|
BundleFormat::Windows => {
|
||||||
|
matches!(
|
||||||
|
package_type,
|
||||||
|
PackageType::WindowsMsi | PackageType::Nsis | PackageType::Updater
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BundleFormat::Android => {
|
||||||
|
matches!(package_type, PackageType::Apk | PackageType::Aab)
|
||||||
|
}
|
||||||
|
BundleFormat::Ios => matches!(package_type, PackageType::IosApp | PackageType::Ipa),
|
||||||
|
BundleFormat::Web | BundleFormat::Server => false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(invalid) = package_types.iter().find(|pt| !is_package_supported(pt)) {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Package type '{invalid:?}' is not supported for bundle format '{bundle}'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Bundle;
|
||||||
|
use crate::{
|
||||||
|
cli::CommandWithPlatformOverrides, BuildArgs, BundleFormat, PackageType, Platform,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ios_bundle_defaults_to_ipa() {
|
||||||
|
assert_eq!(
|
||||||
|
Bundle::default_package_types(BundleFormat::Ios),
|
||||||
|
Some(vec![PackageType::Ipa])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validates_ios_package_types() {
|
||||||
|
assert!(Bundle::validate_package_types_for_bundle(
|
||||||
|
BundleFormat::Ios,
|
||||||
|
Some(&[PackageType::IosApp, PackageType::Ipa]),
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
|
|
||||||
|
assert!(Bundle::validate_package_types_for_bundle(
|
||||||
|
BundleFormat::Ios,
|
||||||
|
Some(&[PackageType::Dmg]),
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bundle_ios_defaults_to_device_target() {
|
||||||
|
let mut bundle = Bundle {
|
||||||
|
package_types: None,
|
||||||
|
out_dir: None,
|
||||||
|
args: CommandWithPlatformOverrides {
|
||||||
|
shared: BuildArgs {
|
||||||
|
build_arguments: crate::TargetArgs {
|
||||||
|
platform: Platform::Ios,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
bundle.force_ios_bundle_device_target_if_needed().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
bundle.args.shared.build_arguments.target,
|
||||||
|
Some("aarch64-apple-ios".parse().unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ async fn check_files_and_report(files_to_check: Vec<PathBuf>) -> Result<()> {
|
|||||||
let res = tokio::spawn(async move {
|
let res = tokio::spawn(async move {
|
||||||
tokio::fs::read_to_string(&_path)
|
tokio::fs::read_to_string(&_path)
|
||||||
.await
|
.await
|
||||||
.map(|contents| dioxus_check::check_file(_path, &contents))
|
.map(|contents| crate::check::check_file(_path, &contents))
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ where
|
|||||||
{
|
{
|
||||||
fn augment_args(cmd: clap::Command) -> clap::Command {
|
fn augment_args(cmd: clap::Command) -> clap::Command {
|
||||||
T::augment_args(cmd).defer(|cmd| {
|
T::augment_args(cmd).defer(|cmd| {
|
||||||
PlatformOverrides::<Self>::augment_subcommands(cmd.disable_help_subcommand(true))
|
PlatformOverrides::<T>::augment_subcommands(cmd.disable_help_subcommand(true))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,3 +182,62 @@ where
|
|||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_parses_bundle_with_package_types_without_recursing() {
|
||||||
|
let cli = crate::Cli::try_parse_from([
|
||||||
|
"dx",
|
||||||
|
"bundle",
|
||||||
|
"--platform",
|
||||||
|
"desktop",
|
||||||
|
"--package-types",
|
||||||
|
"msi",
|
||||||
|
])
|
||||||
|
.expect("bundle command should parse");
|
||||||
|
|
||||||
|
assert!(matches!(cli.action, crate::Commands::Bundle(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_parses_platform_override_subcommands_without_recursing() {
|
||||||
|
let cli = crate::Cli::try_parse_from([
|
||||||
|
"dx",
|
||||||
|
"bundle",
|
||||||
|
"--platform",
|
||||||
|
"desktop",
|
||||||
|
"@client",
|
||||||
|
"--release",
|
||||||
|
"@server",
|
||||||
|
"--platform",
|
||||||
|
"server",
|
||||||
|
])
|
||||||
|
.expect("platform override subcommands should parse");
|
||||||
|
|
||||||
|
let crate::Commands::Bundle(bundle) = cli.action else {
|
||||||
|
panic!("expected bundle command");
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = bundle
|
||||||
|
.args
|
||||||
|
.client
|
||||||
|
.as_ref()
|
||||||
|
.expect("@client override should be captured");
|
||||||
|
let server = bundle
|
||||||
|
.args
|
||||||
|
.server
|
||||||
|
.as_ref()
|
||||||
|
.expect("@server override should be captured");
|
||||||
|
|
||||||
|
assert!(!bundle.args.shared.build_arguments.release);
|
||||||
|
assert!(client.build_arguments.release);
|
||||||
|
assert_eq!(
|
||||||
|
server.build_arguments.platform,
|
||||||
|
crate::Platform::Server,
|
||||||
|
"@server should override the platform"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -178,8 +178,6 @@ impl Print {
|
|||||||
///
|
///
|
||||||
/// This function comes from shell-escape
|
/// This function comes from shell-escape
|
||||||
fn escape_windows(s: Cow<str>) -> Cow<str> {
|
fn escape_windows(s: Cow<str>) -> Cow<str> {
|
||||||
use std::iter::repeat;
|
|
||||||
|
|
||||||
let mut needs_escape = s.is_empty();
|
let mut needs_escape = s.is_empty();
|
||||||
for ch in s.chars() {
|
for ch in s.chars() {
|
||||||
match ch {
|
match ch {
|
||||||
@@ -202,15 +200,15 @@ impl Print {
|
|||||||
|
|
||||||
match chars.next() {
|
match chars.next() {
|
||||||
Some('"') => {
|
Some('"') => {
|
||||||
es.extend(repeat('\\').take(nslashes * 2 + 1));
|
es.extend(std::iter::repeat_n('\\', nslashes * 2 + 1));
|
||||||
es.push('"');
|
es.push('"');
|
||||||
}
|
}
|
||||||
Some(c) => {
|
Some(c) => {
|
||||||
es.extend(repeat('\\').take(nslashes));
|
es.extend(std::iter::repeat_n('\\', nslashes));
|
||||||
es.push(c);
|
es.push(c);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
es.extend(repeat('\\').take(nslashes * 2));
|
es.extend(std::iter::repeat_n('\\', nslashes * 2));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,10 +82,13 @@ impl RunArgs {
|
|||||||
current,
|
current,
|
||||||
total,
|
total,
|
||||||
krate,
|
krate,
|
||||||
|
fresh,
|
||||||
} => {
|
} => {
|
||||||
tracing::debug!(
|
if !fresh {
|
||||||
"[{bundle_format}] ({current}/{total}) Compiling {krate} ",
|
tracing::debug!(
|
||||||
)
|
"[{bundle_format}] ({current}/{total}) Compiling {krate} ",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BuildStage::RunningBindgen => {
|
BuildStage::RunningBindgen => {
|
||||||
tracing::info!("[{bundle_format}] Running WASM bindgen")
|
tracing::info!("[{bundle_format}] Running WASM bindgen")
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ pub(crate) struct WixSettings {
|
|||||||
pub(crate) fips_compliant: bool,
|
pub(crate) fips_compliant: bool,
|
||||||
/// MSI installer version in the format `major.minor.patch.build` (build is optional).
|
/// MSI installer version in the format `major.minor.patch.build` (build is optional).
|
||||||
///
|
///
|
||||||
/// Because a valid version is required for MSI installer, it will be derived from [`tauri_bundler::PackageSettings::version`] if this field is not set.
|
/// Because a valid version is required for MSI installer, it will be derived from the package version if this field is not set.
|
||||||
///
|
///
|
||||||
/// The first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255.
|
/// The first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255.
|
||||||
/// The third and fourth fields have a maximum value of 65,535.
|
/// The third and fourth fields have a maximum value of 65,535.
|
||||||
@@ -132,10 +132,9 @@ pub(crate) struct WixSettings {
|
|||||||
/// A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**,
|
/// A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**,
|
||||||
/// otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app.
|
/// otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app.
|
||||||
///
|
///
|
||||||
/// By default, tauri generates this code by generating a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace.
|
/// By default, this code is generated as a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace.
|
||||||
/// You can use Tauri's CLI to generate and print this code for you by running `tauri inspect wix-upgrade-code`.
|
|
||||||
///
|
///
|
||||||
/// It is recommended that you set this value in your tauri config file to avoid accidental changes in your upgrade code
|
/// It is recommended that you set this value in your Dioxus.toml to avoid accidental changes in your upgrade code
|
||||||
/// whenever you want to change your product name.
|
/// whenever you want to change your product name.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[schemars(with = "Option<String>")]
|
#[schemars(with = "Option<String>")]
|
||||||
@@ -146,28 +145,39 @@ pub(crate) struct WixSettings {
|
|||||||
pub(crate) struct MacOsSettings {
|
pub(crate) struct MacOsSettings {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) bundle_version: Option<String>,
|
pub(crate) bundle_version: Option<String>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) frameworks: Option<Vec<String>>,
|
pub(crate) frameworks: Option<Vec<String>>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) minimum_system_version: Option<String>,
|
pub(crate) minimum_system_version: Option<String>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) license: Option<String>,
|
pub(crate) license: Option<String>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) exception_domain: Option<String>,
|
pub(crate) exception_domain: Option<String>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) signing_identity: Option<String>,
|
pub(crate) signing_identity: Option<String>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) provider_short_name: Option<String>,
|
pub(crate) provider_short_name: Option<String>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) entitlements: Option<String>,
|
pub(crate) entitlements: Option<String>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) info_plist_path: Option<PathBuf>,
|
pub(crate) info_plist_path: Option<PathBuf>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) bundle_name: Option<String>,
|
pub(crate) bundle_name: Option<String>,
|
||||||
|
|
||||||
/// List of custom files to add to the application bundle.
|
/// List of custom files to add to the application bundle.
|
||||||
/// Maps the path in the Contents directory in the app to the path of the file to include (relative to the current working directory).
|
/// Maps the path in the Contents directory in the app to the path of the file to include (relative to the current working directory).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub files: HashMap<PathBuf, PathBuf>,
|
pub files: HashMap<PathBuf, PathBuf>,
|
||||||
|
|
||||||
/// Preserve the hardened runtime version flag, see <https://developer.apple.com/documentation/security/hardened_runtime>
|
/// Preserve the hardened runtime version flag, see <https://developer.apple.com/documentation/security/hardened_runtime>
|
||||||
///
|
///
|
||||||
/// Settings this to `false` is useful when using an ad-hoc signature, making it less strict.
|
/// Settings this to `false` is useful when using an ad-hoc signature, making it less strict.
|
||||||
@@ -290,15 +300,19 @@ pub struct CustomSignCommandSettings {
|
|||||||
pub args: Vec<String>,
|
pub args: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, clap::ValueEnum, Serialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum, Serialize)]
|
||||||
pub(crate) enum PackageType {
|
pub(crate) enum PackageType {
|
||||||
/// The macOS application bundle (.app).
|
/// The macOS application bundle (.app).
|
||||||
#[clap(name = "macos")]
|
#[clap(name = "macos")]
|
||||||
MacOsBundle,
|
MacOsBundle,
|
||||||
|
|
||||||
/// The iOS app bundle.
|
/// The raw iOS application bundle (.app).
|
||||||
#[clap(name = "ios")]
|
#[clap(name = "ios")]
|
||||||
IosBundle,
|
IosApp,
|
||||||
|
|
||||||
|
/// The distributable iOS application archive (.ipa).
|
||||||
|
#[clap(name = "ipa")]
|
||||||
|
Ipa,
|
||||||
|
|
||||||
/// The Windows bundle (.msi).
|
/// The Windows bundle (.msi).
|
||||||
#[clap(name = "msi")]
|
#[clap(name = "msi")]
|
||||||
@@ -327,6 +341,14 @@ pub(crate) enum PackageType {
|
|||||||
/// The Updater bundle (a patch of an existing app)
|
/// The Updater bundle (a patch of an existing app)
|
||||||
#[clap(name = "updater")]
|
#[clap(name = "updater")]
|
||||||
Updater,
|
Updater,
|
||||||
|
|
||||||
|
/// Android application package (.apk).
|
||||||
|
#[clap(name = "apk")]
|
||||||
|
Apk,
|
||||||
|
|
||||||
|
/// Android App Bundle (.aab).
|
||||||
|
#[clap(name = "aab")]
|
||||||
|
Aab,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for PackageType {
|
impl FromStr for PackageType {
|
||||||
@@ -335,7 +357,8 @@ impl FromStr for PackageType {
|
|||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s {
|
match s {
|
||||||
"macos" => Ok(PackageType::MacOsBundle),
|
"macos" => Ok(PackageType::MacOsBundle),
|
||||||
"ios" => Ok(PackageType::IosBundle),
|
"ios" => Ok(PackageType::IosApp),
|
||||||
|
"ipa" => Ok(PackageType::Ipa),
|
||||||
"msi" => Ok(PackageType::WindowsMsi),
|
"msi" => Ok(PackageType::WindowsMsi),
|
||||||
"nsis" => Ok(PackageType::Nsis),
|
"nsis" => Ok(PackageType::Nsis),
|
||||||
"deb" => Ok(PackageType::Deb),
|
"deb" => Ok(PackageType::Deb),
|
||||||
@@ -343,6 +366,8 @@ impl FromStr for PackageType {
|
|||||||
"appimage" => Ok(PackageType::AppImage),
|
"appimage" => Ok(PackageType::AppImage),
|
||||||
"dmg" => Ok(PackageType::Dmg),
|
"dmg" => Ok(PackageType::Dmg),
|
||||||
"updater" => Ok(PackageType::Updater),
|
"updater" => Ok(PackageType::Updater),
|
||||||
|
"apk" => Ok(PackageType::Apk),
|
||||||
|
"aab" => Ok(PackageType::Aab),
|
||||||
_ => Err(format!("{s} is not a valid package type")),
|
_ => Err(format!("{s} is not a valid package type")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
// The file has been placed there by the build script.
|
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
pub const PKG_VERSION_MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
|
||||||
|
pub const PKG_VERSION_MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");
|
||||||
|
pub const PKG_VERSION_PATCH: &str = env!("CARGO_PKG_VERSION_PATCH");
|
||||||
|
pub const PKG_VERSION_PRE: &str = env!("CARGO_PKG_VERSION_PRE");
|
||||||
|
|
||||||
|
pub const GIT_COMMIT_HASH: Option<&str> = option_env!("DIOXUS_CLI_GIT_SHA");
|
||||||
|
pub const GIT_COMMIT_HASH_SHORT: Option<&str> = option_env!("DIOXUS_CLI_GIT_SHA_SHORT");
|
||||||
@@ -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 itertools::Itertools;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{any::Any, io::Read, str::FromStr, sync::Arc, time::SystemTime};
|
use std::{any::Any, io::Read, pin::Pin, str::FromStr, sync::Arc, time::SystemTime};
|
||||||
use std::{borrow::Cow, sync::OnceLock};
|
use std::{borrow::Cow, sync::OnceLock};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
env,
|
env,
|
||||||
fmt::{Debug, Display, Write as _},
|
fmt::{Debug, Display, Write as _},
|
||||||
sync::atomic::{AtomicBool, Ordering},
|
sync::atomic::{AtomicBool, Ordering},
|
||||||
time::Instant,
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use std::{future::Future, panic::AssertUnwindSafe};
|
use std::{future::Future, panic::AssertUnwindSafe};
|
||||||
use tracing::{field::Visit, Level, Subscriber};
|
use tracing::{field::Visit, Level, Subscriber};
|
||||||
@@ -139,10 +139,9 @@ impl TraceController {
|
|||||||
///
|
///
|
||||||
/// We pass the TraceController around the CLI in a few places, namely the serve command so the TUI
|
/// We pass the TraceController around the CLI in a few places, namely the serve command so the TUI
|
||||||
/// can access things like the logs.
|
/// can access things like the logs.
|
||||||
pub async fn main<F>(run_app: impl FnOnce(Commands, Self) -> F) -> StructuredOutput
|
pub async fn main(
|
||||||
where
|
run_app: impl FnOnce(Commands, Self) -> Pin<Box<dyn Future<Output = Result<StructuredOutput>>>>,
|
||||||
F: Future<Output = Result<StructuredOutput>>,
|
) -> StructuredOutput {
|
||||||
{
|
|
||||||
let args = Cli::parse();
|
let args = Cli::parse();
|
||||||
let tui_active = Arc::new(AtomicBool::new(false));
|
let tui_active = Arc::new(AtomicBool::new(false));
|
||||||
let is_serve_cmd = matches!(args.action, Commands::Serve(_));
|
let is_serve_cmd = matches!(args.action, Commands::Serve(_));
|
||||||
@@ -178,7 +177,7 @@ impl TraceController {
|
|||||||
|
|
||||||
// We complete filter out a few fields that are not relevant to the user, like `dx_src` and `json`
|
// We complete filter out a few fields that are not relevant to the user, like `dx_src` and `json`
|
||||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||||
.with_target(args.verbosity.verbose)
|
.with_target(false)
|
||||||
.fmt_fields(
|
.fmt_fields(
|
||||||
format::debug_fn(move |writer, field, value| {
|
format::debug_fn(move |writer, field, value| {
|
||||||
if field.name() == "json" && !args.verbosity.json_output {
|
if field.name() == "json" && !args.verbosity.json_output {
|
||||||
@@ -521,7 +520,7 @@ impl TraceController {
|
|||||||
error_handled,
|
error_handled,
|
||||||
} = event;
|
} = event;
|
||||||
|
|
||||||
let mut ph_event = posthog_rs::Event::new(action, distinct_id.to_string());
|
let mut ph_event = PosthogEvent::new(action, distinct_id.to_string());
|
||||||
|
|
||||||
// The reporter's fields
|
// The reporter's fields
|
||||||
_ = ph_event.insert_prop("is_ci", CliSettings::is_ci());
|
_ = ph_event.insert_prop("is_ci", CliSettings::is_ci());
|
||||||
@@ -649,7 +648,7 @@ impl TraceController {
|
|||||||
reset_cursor();
|
reset_cursor();
|
||||||
|
|
||||||
// re-emit any remaining messages in case they're useful.
|
// re-emit any remaining messages in case they're useful.
|
||||||
while let Ok(Some(msg)) = self.tui_rx.lock().await.try_next() {
|
while let Ok(msg) = self.tui_rx.lock().await.try_recv() {
|
||||||
let content = match msg.content {
|
let content = match msg.content {
|
||||||
TraceContent::Text(text) => text,
|
TraceContent::Text(text) => text,
|
||||||
TraceContent::Cargo(msg) => msg.message.to_string(),
|
TraceContent::Cargo(msg) => msg.message.to_string(),
|
||||||
@@ -729,7 +728,7 @@ impl TraceController {
|
|||||||
true => {
|
true => {
|
||||||
let mut msgs = self.telemetry_rx.lock().await;
|
let mut msgs = self.telemetry_rx.lock().await;
|
||||||
|
|
||||||
let request_body = std::iter::from_fn(|| msgs.try_next().ok().flatten())
|
let request_body = std::iter::from_fn(|| msgs.try_recv().ok())
|
||||||
.filter_map(|msg| serde_json::to_value(msg).ok())
|
.filter_map(|msg| serde_json::to_value(msg).ok())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
@@ -740,8 +739,7 @@ impl TraceController {
|
|||||||
false => {
|
false => {
|
||||||
let mut msgs = self.telemetry_rx.lock().await;
|
let mut msgs = self.telemetry_rx.lock().await;
|
||||||
|
|
||||||
let msg_list =
|
let msg_list = std::iter::from_fn(|| msgs.try_recv().ok()).collect::<Vec<_>>();
|
||||||
std::iter::from_fn(|| msgs.try_next().ok().flatten()).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if !msg_list.is_empty() {
|
if !msg_list.is_empty() {
|
||||||
let dest = Workspace::dioxus_data_dir()
|
let dest = Workspace::dioxus_data_dir()
|
||||||
@@ -1336,10 +1334,43 @@ impl Default for PrettyUptime {
|
|||||||
|
|
||||||
impl FormatTime for PrettyUptime {
|
impl FormatTime for PrettyUptime {
|
||||||
fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
|
fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
|
||||||
let e = self.epoch.elapsed();
|
Self::write_elapsed(self.epoch.elapsed(), w)
|
||||||
write!(w, "{:4}.{:2}s", e.as_secs(), e.subsec_millis())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PrettyUptime {
|
||||||
|
fn write_elapsed(elapsed: Duration, mut w: impl std::fmt::Write) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
w,
|
||||||
|
"{:3}.{:02}s",
|
||||||
|
elapsed.as_secs(),
|
||||||
|
elapsed.subsec_millis() / 10
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod pretty_uptime_tests {
|
||||||
|
use super::PrettyUptime;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pretty_uptime_pads_centiseconds_to_keep_a_stable_width() {
|
||||||
|
let cases = [
|
||||||
|
(Duration::from_millis(92_993), " 92.99s"),
|
||||||
|
(Duration::from_millis(93_085), " 93.08s"),
|
||||||
|
(Duration::from_millis(93_130), " 93.13s"),
|
||||||
|
(Duration::from_millis(999_999), "999.99s"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (elapsed, expected) in cases {
|
||||||
|
let mut rendered = String::new();
|
||||||
|
PrettyUptime::write_elapsed(elapsed, &mut rendered).unwrap();
|
||||||
|
assert_eq!(rendered, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Run the provided future and wait for it to complete, handling Ctrl-C gracefully.
|
/// Run the provided future and wait for it to complete, handling Ctrl-C gracefully.
|
||||||
///
|
///
|
||||||
/// If ctrl-c is pressed twice, it exits immediately, skipping our telemetry flush.
|
/// If ctrl-c is pressed twice, it exits immediately, skipping our telemetry flush.
|
||||||
@@ -1372,7 +1403,37 @@ async fn run_with_ctrl_c(
|
|||||||
/// We only store non-pii information in telemetry to track issues and performance
|
/// We only store non-pii information in telemetry to track issues and performance
|
||||||
/// across the CLI.
|
/// across the CLI.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct Reporter {
|
struct Reporter {
|
||||||
pub session_id: Uuid,
|
session_id: Uuid,
|
||||||
pub distinct_id: Uuid,
|
distinct_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A minimal PostHog event, replacing the `posthog-rs` crate dependency.
|
||||||
|
/// See <https://posthog.com/docs/api/capture>
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct PosthogEvent {
|
||||||
|
event: String,
|
||||||
|
#[serde(rename = "$distinct_id")]
|
||||||
|
distinct_id: String,
|
||||||
|
properties: std::collections::HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PosthogEvent {
|
||||||
|
fn new(event: impl Into<String>, distinct_id: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
event: event.into(),
|
||||||
|
distinct_id: distinct_id.into(),
|
||||||
|
properties: std::collections::HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_prop<V: Serialize>(
|
||||||
|
&mut self,
|
||||||
|
key: impl Into<String>,
|
||||||
|
value: V,
|
||||||
|
) -> Result<(), serde_json::Error> {
|
||||||
|
self.properties
|
||||||
|
.insert(key.into(), serde_json::to_value(value)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+25
-20
@@ -5,15 +5,18 @@
|
|||||||
#![allow(clippy::doc_overindented_list_items)]
|
#![allow(clippy::doc_overindented_list_items)]
|
||||||
|
|
||||||
mod build;
|
mod build;
|
||||||
mod bundle_utils;
|
mod bundler;
|
||||||
mod cargo_toml;
|
mod cargo_toml;
|
||||||
|
mod check;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod devcfg;
|
mod devcfg;
|
||||||
mod dx_build_info;
|
mod dx_build_info;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod esbuild;
|
||||||
mod fastfs;
|
mod fastfs;
|
||||||
mod logging;
|
mod logging;
|
||||||
|
mod opt;
|
||||||
mod platform;
|
mod platform;
|
||||||
mod rustcwrapper;
|
mod rustcwrapper;
|
||||||
mod serve;
|
mod serve;
|
||||||
@@ -53,25 +56,27 @@ async fn main() -> ExitCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run under the tracing collector so we can capture errors/panics.
|
// Run under the tracing collector so we can capture errors/panics.
|
||||||
let result = TraceController::main(|args, tracer| async move {
|
let result = TraceController::main(|args, tracer| {
|
||||||
match args {
|
Box::pin(async move {
|
||||||
Commands::Serve(opts) => opts.serve(&tracer).await,
|
match args {
|
||||||
Commands::Translate(opts) => opts.translate(),
|
Commands::Serve(opts) => opts.serve(&tracer).await,
|
||||||
Commands::New(opts) => opts.create().await,
|
Commands::Translate(opts) => opts.translate(),
|
||||||
Commands::Init(opts) => opts.init().await,
|
Commands::New(opts) => opts.create().await,
|
||||||
Commands::Config(opts) => opts.config().await,
|
Commands::Init(opts) => opts.init().await,
|
||||||
Commands::Autoformat(opts) => opts.autoformat().await,
|
Commands::Config(opts) => opts.config().await,
|
||||||
Commands::Check(opts) => opts.check().await,
|
Commands::Autoformat(opts) => opts.autoformat().await,
|
||||||
Commands::Build(opts) => opts.build().await,
|
Commands::Check(opts) => opts.check().await,
|
||||||
Commands::Bundle(opts) => opts.bundle().await,
|
Commands::Build(opts) => opts.build().await,
|
||||||
Commands::Run(opts) => opts.run().await,
|
Commands::Bundle(opts) => opts.bundle().await,
|
||||||
Commands::SelfUpdate(opts) => opts.self_update().await,
|
Commands::Run(opts) => opts.run().await,
|
||||||
Commands::Tools(BuildTools::BuildAssets(opts)) => opts.run().await,
|
Commands::SelfUpdate(opts) => opts.self_update().await,
|
||||||
Commands::Tools(BuildTools::HotpatchTip(opts)) => opts.run().await,
|
Commands::Tools(BuildTools::BuildAssets(opts)) => opts.run().await,
|
||||||
Commands::Doctor(opts) => opts.doctor().await,
|
Commands::Tools(BuildTools::HotpatchTip(opts)) => opts.run().await,
|
||||||
Commands::Print(opts) => opts.print().await,
|
Commands::Doctor(opts) => opts.doctor().await,
|
||||||
Commands::Components(opts) => opts.run().await,
|
Commands::Print(opts) => opts.print().await,
|
||||||
}
|
Commands::Components(opts) => opts.run().await,
|
||||||
|
}
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// Print the structured output in JSON format for third-party tools to consume.
|
// Print the structured output in JSON format for third-party tools to consume.
|
||||||
|
|||||||
File renamed without changes.
@@ -3,7 +3,7 @@ use manganis::{AssetOptions, CssModuleAssetOptions, FolderAssetOptions};
|
|||||||
use manganis_core::{AssetVariant, CssAssetOptions, ImageAssetOptions, JsAssetOptions};
|
use manganis_core::{AssetVariant, CssAssetOptions, ImageAssetOptions, JsAssetOptions};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::css::{process_css_module, process_scss};
|
use crate::opt::css::{process_css_module, process_scss};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
css::process_css, folder::process_folder, image::process_image, js::process_js,
|
css::process_css, folder::process_folder, image::process_image, js::process_js,
|
||||||
@@ -11,12 +11,13 @@ use super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Process a specific file asset with the given options reading from the source and writing to the output path
|
/// Process a specific file asset with the given options reading from the source and writing to the output path
|
||||||
pub fn process_file_to(
|
pub(crate) fn process_file_to(
|
||||||
options: &AssetOptions,
|
options: &AssetOptions,
|
||||||
source: &Path,
|
source: &Path,
|
||||||
output_path: &Path,
|
output_path: &Path,
|
||||||
|
esbuild_path: Option<&Path>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
process_file_to_with_options(options, source, output_path, false)
|
process_file_to_with_options(options, source, output_path, false, esbuild_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process a specific file asset with additional options
|
/// Process a specific file asset with additional options
|
||||||
@@ -25,6 +26,7 @@ pub(crate) fn process_file_to_with_options(
|
|||||||
source: &Path,
|
source: &Path,
|
||||||
output_path: &Path,
|
output_path: &Path,
|
||||||
in_folder: bool,
|
in_folder: bool,
|
||||||
|
esbuild_path: Option<&Path>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// If the file already exists and this is a hashed asset, then we must have a file
|
// If the file already exists and this is a hashed asset, then we must have a file
|
||||||
// with the same hash already. The hash has the file contents and options, so if we
|
// with the same hash already. The hash has the file contents and options, so if we
|
||||||
@@ -61,7 +63,7 @@ pub(crate) fn process_file_to_with_options(
|
|||||||
process_scss(options, source, &temp_path)?;
|
process_scss(options, source, &temp_path)?;
|
||||||
}
|
}
|
||||||
ResolvedAssetType::Js(options) => {
|
ResolvedAssetType::Js(options) => {
|
||||||
process_js(options, source, &temp_path, !in_folder)?;
|
process_js(options, source, &temp_path, !in_folder, esbuild_path)?;
|
||||||
}
|
}
|
||||||
ResolvedAssetType::Image(options) => {
|
ResolvedAssetType::Image(options) => {
|
||||||
process_image(options, source, &temp_path)?;
|
process_image(options, source, &temp_path)?;
|
||||||
@@ -70,7 +72,7 @@ pub(crate) fn process_file_to_with_options(
|
|||||||
process_json(source, &temp_path)?;
|
process_json(source, &temp_path)?;
|
||||||
}
|
}
|
||||||
ResolvedAssetType::Folder(_) => {
|
ResolvedAssetType::Folder(_) => {
|
||||||
process_folder(source, &temp_path)?;
|
process_folder(source, &temp_path, esbuild_path)?;
|
||||||
}
|
}
|
||||||
ResolvedAssetType::File => {
|
ResolvedAssetType::File => {
|
||||||
let source_file = std::fs::File::open(source)?;
|
let source_file = std::fs::File::open(source)?;
|
||||||
@@ -2,10 +2,14 @@ use std::path::Path;
|
|||||||
|
|
||||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||||
|
|
||||||
use crate::file::process_file_to_with_options;
|
use crate::opt::file::process_file_to_with_options;
|
||||||
|
|
||||||
/// Process a folder, optimizing and copying all assets into the output folder
|
/// Process a folder, optimizing and copying all assets into the output folder
|
||||||
pub fn process_folder(source: &Path, output_folder: &Path) -> anyhow::Result<()> {
|
pub fn process_folder(
|
||||||
|
source: &Path,
|
||||||
|
output_folder: &Path,
|
||||||
|
esbuild_path: Option<&Path>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
// Create the folder
|
// Create the folder
|
||||||
std::fs::create_dir_all(output_folder)?;
|
std::fs::create_dir_all(output_folder)?;
|
||||||
|
|
||||||
@@ -21,9 +25,9 @@ pub fn process_folder(source: &Path, output_folder: &Path) -> anyhow::Result<()>
|
|||||||
let metadata = file.metadata()?;
|
let metadata = file.metadata()?;
|
||||||
let output_path = output_folder.join(file.strip_prefix(source)?);
|
let output_path = output_folder.join(file.strip_prefix(source)?);
|
||||||
if metadata.is_dir() {
|
if metadata.is_dir() {
|
||||||
process_folder(&file, &output_path)
|
process_folder(&file, &output_path, esbuild_path)
|
||||||
} else {
|
} else {
|
||||||
process_file_minimal(&file, &output_path)
|
process_file_minimal(&file, &output_path, esbuild_path)
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -31,12 +35,17 @@ pub fn process_folder(source: &Path, output_folder: &Path) -> anyhow::Result<()>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Optimize a file without changing any of its contents significantly (e.g. by changing the extension)
|
/// Optimize a file without changing any of its contents significantly (e.g. by changing the extension)
|
||||||
fn process_file_minimal(input_path: &Path, output_path: &Path) -> anyhow::Result<()> {
|
fn process_file_minimal(
|
||||||
|
input_path: &Path,
|
||||||
|
output_path: &Path,
|
||||||
|
esbuild_path: Option<&Path>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
process_file_to_with_options(
|
process_file_to_with_options(
|
||||||
&manganis_core::AssetOptions::builder().into_asset_options(),
|
&manganis_core::AssetOptions::builder().into_asset_options(),
|
||||||
input_path,
|
input_path,
|
||||||
output_path,
|
output_path,
|
||||||
true,
|
true,
|
||||||
|
esbuild_path,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::opt::{
|
||||||
css::hash_scss,
|
css::hash_scss,
|
||||||
file::{resolve_asset_options, ResolvedAssetType},
|
file::{resolve_asset_options, ResolvedAssetType},
|
||||||
js::hash_js,
|
js::hash_js,
|
||||||
@@ -48,8 +48,8 @@ fn hash_file(options: &AssetOptions, source: &Path) -> anyhow::Result<AssetHash>
|
|||||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||||
options.hash(&mut hash);
|
options.hash(&mut hash);
|
||||||
|
|
||||||
// Hash the version of CLI opt
|
// Hash the version of the CLI
|
||||||
hash.write(crate::build_info::version().as_bytes());
|
hash.write(crate::dx_build_info::PKG_VERSION.as_bytes());
|
||||||
hash_file_with_options(options, source, &mut hash, false)?;
|
hash_file_with_options(options, source, &mut hash, false)?;
|
||||||
|
|
||||||
let hash = hash.finish();
|
let hash = hash.finish();
|
||||||
@@ -128,7 +128,7 @@ pub(crate) fn hash_file_contents(source: &Path, hasher: &mut impl Hasher) -> any
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Add a hash to the asset, or log an error if it fails
|
/// Add a hash to the asset, or log an error if it fails
|
||||||
pub fn add_hash_to_asset(asset: &mut BundledAsset) {
|
pub(crate) fn add_hash_to_asset(asset: &mut BundledAsset) {
|
||||||
let source = asset.absolute_source_path();
|
let source = asset.absolute_source_path();
|
||||||
match AssetHash::hash_file_contents(asset.options(), source) {
|
match AssetHash::hash_file_contents(asset.options(), source) {
|
||||||
Ok(hash) => {
|
Ok(hash) => {
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use jpg::compress_jpg;
|
use image::{DynamicImage, EncodableLayout};
|
||||||
use manganis_core::{ImageAssetOptions, ImageFormat, ImageSize};
|
use manganis_core::{ImageAssetOptions, ImageFormat, ImageSize};
|
||||||
use png::compress_png;
|
use std::io::{BufWriter, Write};
|
||||||
|
use std::path::Path;
|
||||||
mod jpg;
|
|
||||||
mod png;
|
|
||||||
|
|
||||||
pub(crate) fn process_image(
|
pub(crate) fn process_image(
|
||||||
image_options: &ImageAssetOptions,
|
image_options: &ImageAssetOptions,
|
||||||
@@ -71,3 +67,70 @@ pub(crate) fn process_image(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn compress_png(image: DynamicImage, output_location: &Path) {
|
||||||
|
// Image loading/saving is outside scope of this library
|
||||||
|
let width = image.width() as usize;
|
||||||
|
let height = image.height() as usize;
|
||||||
|
let bitmap: Vec<_> = image
|
||||||
|
.into_rgba8()
|
||||||
|
.pixels()
|
||||||
|
.map(|px| imagequant::RGBA::new(px[0], px[1], px[2], px[3]))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Configure the library
|
||||||
|
let mut liq = imagequant::new();
|
||||||
|
liq.set_speed(5).unwrap();
|
||||||
|
liq.set_quality(0, 99).unwrap();
|
||||||
|
|
||||||
|
// Describe the bitmap
|
||||||
|
let mut img = liq.new_image(&bitmap[..], width, height, 0.0).unwrap();
|
||||||
|
|
||||||
|
// The magic happens in quantize()
|
||||||
|
let mut res = match liq.quantize(&mut img) {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(err) => panic!("Quantization failed, because: {err:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (palette, pixels) = res.remapped(&mut img).unwrap();
|
||||||
|
|
||||||
|
let file = std::fs::File::create(output_location).unwrap();
|
||||||
|
let w = &mut BufWriter::new(file);
|
||||||
|
|
||||||
|
let mut encoder = png::Encoder::new(w, width as u32, height as u32);
|
||||||
|
encoder.set_color(png::ColorType::Rgba);
|
||||||
|
let mut flattened_palette = Vec::new();
|
||||||
|
let mut alpha_palette = Vec::new();
|
||||||
|
for px in palette {
|
||||||
|
flattened_palette.push(px.r);
|
||||||
|
flattened_palette.push(px.g);
|
||||||
|
flattened_palette.push(px.b);
|
||||||
|
alpha_palette.push(px.a);
|
||||||
|
}
|
||||||
|
encoder.set_palette(flattened_palette);
|
||||||
|
encoder.set_trns(alpha_palette);
|
||||||
|
encoder.set_depth(png::BitDepth::Eight);
|
||||||
|
encoder.set_color(png::ColorType::Indexed);
|
||||||
|
encoder.set_compression(png::Compression::Best);
|
||||||
|
let mut writer = encoder.write_header().unwrap();
|
||||||
|
writer.write_image_data(&pixels).unwrap();
|
||||||
|
writer.finish().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn compress_jpg(image: DynamicImage, output_location: &Path) -> anyhow::Result<()> {
|
||||||
|
let mut comp = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_EXT_RGBX);
|
||||||
|
let width = image.width() as usize;
|
||||||
|
let height = image.height() as usize;
|
||||||
|
|
||||||
|
comp.set_size(width, height);
|
||||||
|
let mut comp = comp.start_compress(Vec::new())?; // any io::Write will work
|
||||||
|
|
||||||
|
comp.write_scanlines(image.to_rgba8().as_bytes())?;
|
||||||
|
|
||||||
|
let jpeg_bytes = comp.finish()?;
|
||||||
|
|
||||||
|
let file = std::fs::File::create(output_location)?;
|
||||||
|
let w = &mut BufWriter::new(file);
|
||||||
|
w.write_all(&jpeg_bytes)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use manganis_core::JsAssetOptions;
|
||||||
|
|
||||||
|
use crate::opt::hash::hash_file_contents;
|
||||||
|
|
||||||
|
pub(crate) fn process_js(
|
||||||
|
js_options: &JsAssetOptions,
|
||||||
|
source: &Path,
|
||||||
|
output_path: &Path,
|
||||||
|
bundle: bool,
|
||||||
|
esbuild_path: Option<&Path>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let minify = js_options.minified();
|
||||||
|
let needs_esbuild = minify || bundle;
|
||||||
|
|
||||||
|
if needs_esbuild {
|
||||||
|
if let Some(esbuild) = esbuild_path {
|
||||||
|
match run_esbuild(esbuild, source, output_path, bundle, minify) {
|
||||||
|
Ok(()) => return Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to process JS with esbuild. Falling back to copy: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::warn!("esbuild binary path not set. Copying JS without processing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: copy unprocessed
|
||||||
|
let mut source_file = std::fs::File::open(source)?;
|
||||||
|
let mut writer = std::io::BufWriter::new(std::fs::File::create(output_path)?);
|
||||||
|
std::io::copy(&mut source_file, &mut writer).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to write JS to output location: {}",
|
||||||
|
output_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run esbuild to bundle and/or minify a JavaScript file.
|
||||||
|
fn run_esbuild(
|
||||||
|
esbuild: &Path,
|
||||||
|
source: &Path,
|
||||||
|
output_path: &Path,
|
||||||
|
bundle: bool,
|
||||||
|
minify: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut cmd = std::process::Command::new(esbuild);
|
||||||
|
cmd.arg(source);
|
||||||
|
cmd.arg(format!("--outfile={}", output_path.display()));
|
||||||
|
cmd.arg("--log-level=warning");
|
||||||
|
|
||||||
|
if bundle {
|
||||||
|
cmd.arg("--bundle");
|
||||||
|
cmd.arg("--format=esm");
|
||||||
|
}
|
||||||
|
|
||||||
|
if minify {
|
||||||
|
cmd.arg("--minify");
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!("Running esbuild: {:?}", cmd);
|
||||||
|
|
||||||
|
let output = cmd.output().context("Failed to run esbuild")?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
anyhow::bail!("esbuild failed: {stderr}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn hash_js(
|
||||||
|
_js_options: &JsAssetOptions,
|
||||||
|
source: &Path,
|
||||||
|
hasher: &mut impl std::hash::Hasher,
|
||||||
|
_bundle: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
hash_file_contents(source, hasher)
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -1,13 +1,8 @@
|
|||||||
use anyhow::Context;
|
|
||||||
use manganis::AssetOptions;
|
|
||||||
use manganis_core::BundledAsset;
|
use manganis_core::BundledAsset;
|
||||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{BTreeMap, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
|
|
||||||
mod build_info;
|
|
||||||
mod css;
|
mod css;
|
||||||
mod file;
|
mod file;
|
||||||
mod folder;
|
mod folder;
|
||||||
@@ -16,14 +11,14 @@ mod image;
|
|||||||
mod js;
|
mod js;
|
||||||
mod json;
|
mod json;
|
||||||
|
|
||||||
pub use file::process_file_to;
|
pub(crate) use file::process_file_to;
|
||||||
pub use hash::add_hash_to_asset;
|
pub(crate) use hash::add_hash_to_asset;
|
||||||
|
|
||||||
/// A manifest of all assets collected from dependencies
|
/// A manifest of all assets collected from dependencies
|
||||||
///
|
///
|
||||||
/// This will be filled in primarily by incremental compilation artifacts.
|
/// This will be filled in primarily by incremental compilation artifacts.
|
||||||
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
|
||||||
pub struct AssetManifest {
|
pub(crate) struct AssetManifest {
|
||||||
/// Map of bundled asset name to the asset itself
|
/// Map of bundled asset name to the asset itself
|
||||||
assets: BTreeMap<PathBuf, HashSet<BundledAsset>>,
|
assets: BTreeMap<PathBuf, HashSet<BundledAsset>>,
|
||||||
}
|
}
|
||||||
@@ -88,41 +83,4 @@ impl AssetManifest {
|
|||||||
.flat_map(|assets| assets.iter())
|
.flat_map(|assets| assets.iter())
|
||||||
.filter(move |asset| seen.insert(asset.bundled_path()))
|
.filter(move |asset| seen.insert(asset.bundled_path()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_from_file(path: &Path) -> anyhow::Result<Self> {
|
|
||||||
let src = std::fs::read_to_string(path)?;
|
|
||||||
|
|
||||||
serde_json::from_str(&src)
|
|
||||||
.with_context(|| format!("Failed to parse asset manifest from {path:?}\n{src}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optimize a list of assets in parallel
|
|
||||||
pub fn optimize_all_assets(
|
|
||||||
assets_to_transfer: Vec<(PathBuf, PathBuf, AssetOptions)>,
|
|
||||||
on_optimization_start: impl FnMut(&Path, &Path, &AssetOptions) + Sync + Send,
|
|
||||||
on_optimization_end: impl FnMut(&Path, &Path, &AssetOptions) + Sync + Send,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let on_optimization_start = Arc::new(RwLock::new(on_optimization_start));
|
|
||||||
let on_optimization_end = Arc::new(RwLock::new(on_optimization_end));
|
|
||||||
assets_to_transfer
|
|
||||||
.par_iter()
|
|
||||||
.try_for_each(|(from, to, options)| {
|
|
||||||
{
|
|
||||||
let mut on_optimization_start = on_optimization_start.write().unwrap();
|
|
||||||
on_optimization_start(from, to, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = process_file_to(options, from, to);
|
|
||||||
if let Err(err) = res.as_ref() {
|
|
||||||
tracing::error!("Failed to copy asset {from:?}: {err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut on_optimization_end = on_optimization_end.write().unwrap();
|
|
||||||
on_optimization_end(from, to, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.map(|_| ())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
@@ -439,6 +439,7 @@ impl Output {
|
|||||||
/// Render the current state of everything to the console screen
|
/// Render the current state of everything to the console screen
|
||||||
pub fn render(&mut self, runner: &AppServer, server: &WebServer) {
|
pub fn render(&mut self, runner: &AppServer, server: &WebServer) {
|
||||||
if !self.interactive {
|
if !self.interactive {
|
||||||
|
self.emit_logs_to_tracing();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1094,11 +1095,31 @@ impl Output {
|
|||||||
|
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drains the current log queue to tracing so they still get printed even if the output is non-interactive
|
||||||
|
fn emit_logs_to_tracing(&mut self) {
|
||||||
|
// In non-interactive mode (CI, piped output), drain pending logs to tracing
|
||||||
|
// so they still appear in stdout/stderr instead of being silently dropped.
|
||||||
|
while let Some(log) = self.pending_logs.pop_back() {
|
||||||
|
let msg = match &log.content {
|
||||||
|
TraceContent::Text(s) => s.as_str(),
|
||||||
|
TraceContent::Cargo(d) => d.message.as_str(),
|
||||||
|
};
|
||||||
|
match log.level {
|
||||||
|
Level::ERROR => tracing::error!("{msg}"),
|
||||||
|
Level::WARN => tracing::warn!("{msg}"),
|
||||||
|
Level::INFO => tracing::info!("{msg}"),
|
||||||
|
Level::DEBUG => tracing::debug!("{msg}"),
|
||||||
|
Level::TRACE => tracing::trace!("{msg}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Drop for Output {
|
impl std::ops::Drop for Output {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if !self.interactive {
|
if !self.interactive {
|
||||||
|
self.emit_logs_to_tracing();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ impl AppServer {
|
|||||||
let mut changes: Vec<_> = event.into_iter().collect();
|
let mut changes: Vec<_> = event.into_iter().collect();
|
||||||
|
|
||||||
// Dequeue in bulk if we can, we might've received a lot of events in one go
|
// Dequeue in bulk if we can, we might've received a lot of events in one go
|
||||||
while let Some(event) = self.watcher_rx.try_next().ok().flatten() {
|
while let Ok(event) = self.watcher_rx.try_recv() {
|
||||||
changes.push(event);
|
changes.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1313,7 +1313,7 @@ impl AppServer {
|
|||||||
let depth = relative.components().count();
|
let depth = relative.components().count();
|
||||||
let is_better = best_match
|
let is_better = best_match
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(true, |(_, best_depth)| depth < *best_depth);
|
.is_none_or(|(_, best_depth)| depth < *best_depth);
|
||||||
if is_better {
|
if is_better {
|
||||||
best_match = Some((krate.name.replace('-', "_"), depth));
|
best_match = Some((krate.name.replace('-', "_"), depth));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use futures_util::{
|
|||||||
StreamExt,
|
StreamExt,
|
||||||
};
|
};
|
||||||
use hyper::HeaderMap;
|
use hyper::HeaderMap;
|
||||||
use rustls::crypto::{aws_lc_rs::default_provider, CryptoProvider};
|
use rustls::crypto::{ring::default_provider, CryptoProvider};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
|
|||||||
@@ -135,15 +135,15 @@ async fn find_latest_wasm_opt_download_url() -> anyhow::Result<String> {
|
|||||||
// Find the platform identifier based on the current OS and architecture
|
// Find the platform identifier based on the current OS and architecture
|
||||||
// hardcoded for now to get around github api rate limits
|
// hardcoded for now to get around github api rate limits
|
||||||
if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
|
if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
|
||||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-x86_64-windows.tar.gz".to_string());
|
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-x86_64-windows.tar.gz".to_string());
|
||||||
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
|
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
|
||||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-x86_64-linux.tar.gz".to_string());
|
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-x86_64-linux.tar.gz".to_string());
|
||||||
} else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
|
} else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
|
||||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-aarch64-linux.tar.gz".to_string());
|
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-aarch64-linux.tar.gz".to_string());
|
||||||
} else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
|
} else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
|
||||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-x86_64-macos.tar.gz".to_string());
|
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-x86_64-macos.tar.gz".to_string());
|
||||||
} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
|
} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
|
||||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-arm64-macos.tar.gz".to_string());
|
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-arm64-macos.tar.gz".to_string());
|
||||||
};
|
};
|
||||||
|
|
||||||
let url = "https://api.github.com/repos/WebAssembly/binaryen/releases/latest";
|
let url = "https://api.github.com/repos/WebAssembly/binaryen/releases/latest";
|
||||||
|
|||||||
@@ -479,7 +479,7 @@ impl VirtualDom {
|
|||||||
/// Queue any pending events
|
/// Queue any pending events
|
||||||
fn queue_events(&mut self) {
|
fn queue_events(&mut self) {
|
||||||
// Prevent a task from deadlocking the runtime by repeatedly queueing itself
|
// Prevent a task from deadlocking the runtime by repeatedly queueing itself
|
||||||
while let Ok(Some(msg)) = self.rx.try_next() {
|
while let Ok(msg) = self.rx.try_recv() {
|
||||||
match msg {
|
match msg {
|
||||||
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
|
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
|
||||||
SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)),
|
SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)),
|
||||||
|
|||||||
@@ -131,9 +131,16 @@ impl Config {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// set the directory where data will be stored in release mode.
|
/// Set the directory where WebView2 stores its user data (cookies, cache, IndexedDB, etc.).
|
||||||
///
|
///
|
||||||
/// > Note: This **must** be set when bundling on Windows.
|
/// If not set, Dioxus automatically chooses a sensible default:
|
||||||
|
/// - **Windows:** `%LOCALAPPDATA%/<exe_name>` — this avoids the WebView2 default of
|
||||||
|
/// placing data next to the executable, which fails in read-only locations like
|
||||||
|
/// `Program Files` or on certain drive types (e.g. ReFS dev drives).
|
||||||
|
/// - **macOS/Linux:** managed by WebKit automatically.
|
||||||
|
///
|
||||||
|
/// You typically only need this if you want multiple apps to share a WebView2 profile
|
||||||
|
/// or need the data in a specific location for compliance/IT policy reasons.
|
||||||
pub fn with_data_directory(mut self, path: impl Into<PathBuf>) -> Self {
|
pub fn with_data_directory(mut self, path: impl Into<PathBuf>) -> Self {
|
||||||
self.data_dir = Some(path.into());
|
self.data_dir = Some(path.into());
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ impl EditWebsocket {
|
|||||||
let hex_encoded_server_key = encode_key_string(¤t_server_location.server_key);
|
let hex_encoded_server_key = encode_key_string(¤t_server_location.server_key);
|
||||||
let mut location = None;
|
let mut location = None;
|
||||||
|
|
||||||
|
#[allow(clippy::result_large_err)]
|
||||||
let on_request = |req: &Request, res| {
|
let on_request = |req: &Request, res| {
|
||||||
// Try to parse the webview id and key from the path
|
// Try to parse the webview id and key from the path
|
||||||
let path = req.uri().path();
|
let path = req.uri().path();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
#![allow(unexpected_cfgs)]
|
||||||
|
|
||||||
mod android_sync_lock;
|
mod android_sync_lock;
|
||||||
mod app;
|
mod app;
|
||||||
|
|||||||
@@ -260,12 +260,23 @@ impl WebviewInstance {
|
|||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let window: id = window.ns_window() as id;
|
let window: id = window.ns_window() as id;
|
||||||
#[allow(unexpected_cfgs)]
|
|
||||||
let _: () = msg_send![window, setCollectionBehavior: NSWindowCollectionBehavior::NSWindowCollectionBehaviorManaged];
|
let _: () = msg_send![window, setCollectionBehavior: NSWindowCollectionBehavior::NSWindowCollectionBehaviorManaged];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut web_context = WebContext::new(cfg.data_dir.clone());
|
let mut web_context = WebContext::new(cfg.data_dir.clone().or_else(|| {
|
||||||
|
// On Windows, WebView2 defaults to storing its data next to the executable.
|
||||||
|
// This fails on certain drives (e.g. ReFS dev drives, Program Files) where the
|
||||||
|
// directory may not be writable. Fall back to %LOCALAPPDATA%/<exe_name> automatically.
|
||||||
|
if cfg!(windows) {
|
||||||
|
let exe = std::env::current_exe().ok()?;
|
||||||
|
let name = exe.file_stem()?.to_str()?;
|
||||||
|
let local_app_data = std::env::var("LOCALAPPDATA").ok()?;
|
||||||
|
Some(std::path::PathBuf::from(local_app_data).join(name))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}));
|
||||||
let edit_queue = shared.websocket.create_queue();
|
let edit_queue = shared.websocket.create_queue();
|
||||||
let asset_handlers = AssetHandlerRegistry::new();
|
let asset_handlers = AssetHandlerRegistry::new();
|
||||||
let edits = WebviewEdits::new(dom.runtime(), edit_queue.clone());
|
let edits = WebviewEdits::new(dom.runtime(), edit_queue.clone());
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ pub enum BuildStage {
|
|||||||
current: usize,
|
current: usize,
|
||||||
total: usize,
|
total: usize,
|
||||||
krate: String,
|
krate: String,
|
||||||
|
fresh: bool,
|
||||||
},
|
},
|
||||||
RunningBindgen,
|
RunningBindgen,
|
||||||
SplittingBundle,
|
SplittingBundle,
|
||||||
|
|||||||
@@ -1,251 +1,112 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
const { defineConfig, devices } = require("@playwright/test");
|
const { defineConfig, devices } = require("@playwright/test");
|
||||||
|
const { execSync } = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
/**
|
// Copy a directory to a temp location for tests that modify source files (hot-patch tests).
|
||||||
* Read environment variables from file.
|
// Done in JS so the webServer command is a simple `dx serve` with a `cwd`, which keeps
|
||||||
* https://github.com/motdotla/dotenv
|
// stdout/stderr piping clean on both Unix and Windows cmd.exe.
|
||||||
*/
|
function copyToTemp(src, dest) {
|
||||||
// require('dotenv').config();
|
const absSrc = path.resolve(__dirname, src);
|
||||||
|
const absDest = path.resolve(__dirname, dest);
|
||||||
|
fs.rmSync(absDest, { recursive: true, force: true });
|
||||||
|
fs.cpSync(absSrc, absDest, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
let webServer = [];
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||||
let grep = undefined;
|
const dx = path.join(repoRoot, "target", "release", process.platform === "win32" ? "dx.exe" : "dx");
|
||||||
let grepInvert = undefined;
|
|
||||||
|
const ALL_SERVERS = [
|
||||||
|
{
|
||||||
|
specs: ["liveview.spec.js"], port: 3030,
|
||||||
|
command: "cargo run --package dioxus-playwright-liveview-test --bin dioxus-playwright-liveview-test",
|
||||||
|
env: { CARGO_TERM_PROGRESS_WHEN: "never" }
|
||||||
|
},
|
||||||
|
{ specs: ["web.spec.js"], port: 9990, cwd: "web", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 9990` },
|
||||||
|
{ specs: ["web-routing.spec.js"], port: 2020, cwd: "web-routing", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 2020` },
|
||||||
|
{ specs: ["web-hash-routing.spec.js"], port: 2021, cwd: "web-hash-routing", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 2021` },
|
||||||
|
{ specs: ["fullstack.spec.js"], port: 3333, cwd: "fullstack", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 3333 --release` },
|
||||||
|
{ specs: ["fullstack-errors.spec.js"], port: 3232, cwd: "fullstack-errors", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 3232` },
|
||||||
|
{ specs: ["fullstack-mounted.spec.js"], port: 7777, cwd: "fullstack-mounted", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 7777` },
|
||||||
|
{ specs: ["fullstack-routing.spec.js"], port: 8888, cwd: "fullstack-routing", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 8888` },
|
||||||
|
{ specs: ["fullstack-spread.spec.js"], port: 7980, cwd: "fullstack-spread", command: `${dx} run --verbose --force-sequential --web --addr 127.0.0.1 --port 7980` },
|
||||||
|
{ specs: ["fullstack-hydration-order.spec.js"], port: 7979, cwd: "fullstack-hydration-order", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 7979` },
|
||||||
|
{ specs: ["fullstack-error-codes.spec.js"], port: 8124, cwd: "fullstack-error-codes", command: `${dx} run --force-sequential --addr 127.0.0.1 --port 8124` },
|
||||||
|
{ specs: ["nested-suspense.spec.js", "nested-suspense-no-js.spec.js"], port: 5050, cwd: "nested-suspense", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 5050` },
|
||||||
|
{ specs: ["nested-suspense-ssg.spec.js"], port: 6060, cwd: "nested-suspense", command: `${dx} run --bin nested-suspense-ssg --force-sequential --web --ssg --addr 127.0.0.1 --port 6060` },
|
||||||
|
{ specs: ["suspense-carousel.spec.js"], port: 4040, cwd: "suspense-carousel", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 4040` },
|
||||||
|
{ specs: ["cli-optimization.spec.js"], port: 8989, cwd: "cli-optimization", command: `${dx} run --addr 127.0.0.1 --port 8989` },
|
||||||
|
{ specs: ["wasm-split.spec.js"], port: 8001, cwd: "wasm-split-harness", command: `${dx} run --bin wasm-split-harness --web --addr 127.0.0.1 --port 8001 --wasm-split --profile wasm-split-release` },
|
||||||
|
{ specs: ["default-features-disabled.spec.js"], port: 8002, cwd: "default-features-disabled", command: `${dx} run --force-sequential --addr 127.0.0.1 --port 8002` },
|
||||||
|
{ specs: ["web-patch.spec.js"], port: 9980, cwd: "web-hot-patch-temp", setup: () => copyToTemp("web-hot-patch", "web-hot-patch-temp"), command: `${dx} serve --verbose --force-sequential --web --addr 127.0.0.1 --port 9980 --hot-patch --exit-on-error` },
|
||||||
|
{ specs: ["web-patch-fullstack.spec.js"], port: 9981, cwd: "web-hot-patch-fullstack-temp", setup: () => copyToTemp("web-hot-patch-fullstack", "web-hot-patch-fullstack-temp"), command: `${dx} serve --verbose --force-sequential --web --addr 127.0.0.1 --port 9981 --hot-patch --exit-on-error` },
|
||||||
|
];
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
webServer = [
|
ALL_SERVERS.push({ specs: ["windows.spec.js"], port: 8787, cwd: "windows-headless", command: `${dx} run --force-sequential` });
|
||||||
{
|
ALL_SERVERS.push({ specs: ["windows-hotpatch-fullstack.spec.js"], port: 8788, cwd: "windows-hotpatch-fullstack-temp", setup: () => copyToTemp("windows-hotpatch-fullstack", "windows-hotpatch-fullstack-temp"), command: `${dx} serve --verbose --force-sequential --hot-patch --exit-on-error` });
|
||||||
cwd: path.join(process.cwd(), "windows-headless"),
|
}
|
||||||
command:
|
|
||||||
"cargo run --package dioxus-cli --release -- run --force-sequential",
|
// Determine which servers to start based on spec files in argv and platform.
|
||||||
port: 8787,
|
// On Windows only windows.spec.js runs (grep filter below), so don't bother
|
||||||
timeout: 50 * 60 * 1000,
|
// starting servers for tests that will never execute — they just crash and
|
||||||
reuseExistingServer: !process.env.CI,
|
// pollute the logs.
|
||||||
stdout: "pipe",
|
const specArgs = process.argv.filter((a) => a.endsWith(".spec.js")).map((a) => path.basename(a));
|
||||||
},
|
const isWindows = process.platform === "win32";
|
||||||
];
|
const activeServers = specArgs.length > 0
|
||||||
grep = /windows/;
|
? ALL_SERVERS.filter((s) => s.specs.some((spec) => specArgs.includes(spec)))
|
||||||
} else {
|
: isWindows
|
||||||
webServer = [
|
? ALL_SERVERS.filter((s) => s.specs.some((spec) => spec.includes("windows")))
|
||||||
{
|
: ALL_SERVERS.filter((s) => !s.specs.some((spec) => spec.includes("windows")));
|
||||||
command:
|
|
||||||
"cargo run --package dioxus-playwright-liveview-test --bin dioxus-playwright-liveview-test",
|
// Run any setup functions (e.g. copying source to temp dirs for hot-patch tests)
|
||||||
port: 3030,
|
// Only happens on initialize
|
||||||
timeout: 50 * 60 * 1000,
|
//
|
||||||
reuseExistingServer: !process.env.CI,
|
// Build dx once: main process builds and sets env var; workers inherit it and skip.
|
||||||
stdout: "pipe",
|
// todo: implement proper fixtures https://www.youtube.com/watch?v=3i6cJUFO_m4
|
||||||
},
|
if (!process.env._DX_BUILT) {
|
||||||
{
|
for (const s of activeServers) {
|
||||||
cwd: path.join(process.cwd(), "web"),
|
if (s.setup) s.setup();
|
||||||
command:
|
}
|
||||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 9990',
|
|
||||||
port: 9990,
|
execSync("cargo build --package dioxus-cli --release", {
|
||||||
timeout: 50 * 60 * 1000,
|
cwd: repoRoot,
|
||||||
reuseExistingServer: !process.env.CI,
|
env: { ...process.env, CARGO_TERM_PROGRESS_WHEN: "never" },
|
||||||
stdout: "pipe",
|
stdio: "inherit",
|
||||||
},
|
});
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "web-routing"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 2020',
|
|
||||||
port: 2020,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "web-hash-routing"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 2021',
|
|
||||||
port: 2021,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "fullstack"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 3333 --release',
|
|
||||||
port: 3333,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "fullstack-errors"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 3232',
|
|
||||||
port: 3232,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "fullstack-mounted"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 7777',
|
|
||||||
port: 7777,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "fullstack-spread"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 7980',
|
|
||||||
port: 7980,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "fullstack-routing"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 8888',
|
|
||||||
port: 8888,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "suspense-carousel"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 4040',
|
|
||||||
port: 4040,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "nested-suspense"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 5050',
|
|
||||||
port: 5050,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "nested-suspense"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --bin nested-suspense-ssg --force-sequential --web --ssg --addr "127.0.0.1" --port 6060',
|
|
||||||
port: 6060,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "fullstack-hydration-order"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 7979',
|
|
||||||
port: 7979,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "cli-optimization"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --addr "127.0.0.1" --port 8989',
|
|
||||||
port: 8989,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "wasm-split-harness"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --bin wasm-split-harness --web --addr "127.0.0.1" --port 8001 --wasm-split --profile wasm-split-release',
|
|
||||||
port: 8001,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "default-features-disabled"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --force-sequential --addr "127.0.0.1" --port 8002',
|
|
||||||
port: 8002,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "barebones-template"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --force-sequential --addr "127.0.0.1" --port 8123',
|
|
||||||
port: 8123,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command:
|
|
||||||
'rm -rf ./web-hot-patch-temp && cp -r ./web-hot-patch ./web-hot-patch-temp && cd web-hot-patch-temp && cargo run --manifest-path ../../cli/Cargo.toml --release -- serve --verbose --force-sequential --web --addr "127.0.0.1" --port 9980 --hot-patch --exit-on-error',
|
|
||||||
port: 9980,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command:
|
|
||||||
'rm -rf ./web-hot-patch-fullstack-temp && cp -r ./web-hot-patch-fullstack ./web-hot-patch-fullstack-temp && cd web-hot-patch-fullstack-temp && cargo run --manifest-path ../../cli/Cargo.toml --release -- serve --verbose --force-sequential --web --addr "127.0.0.1" --port 9981 --hot-patch --exit-on-error',
|
|
||||||
port: 9981,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cwd: path.join(process.cwd(), "fullstack-error-codes"),
|
|
||||||
command:
|
|
||||||
'cargo run --package dioxus-cli --release -- run --force-sequential --addr "127.0.0.1" --port 8124',
|
|
||||||
port: 8124,
|
|
||||||
timeout: 50 * 60 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
stdout: "pipe",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
grepInvert = /windows/;
|
process.env._DX_BUILT = "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @see https://playwright.dev/docs/test-configuration
|
|
||||||
*/
|
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
testDir: ".",
|
testDir: ".",
|
||||||
/* Run tests in files in parallel */
|
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
/* Retry on CI only */
|
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
|
||||||
workers: process.env.CI ? 1 : undefined,
|
workers: process.env.CI ? 1 : undefined,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
||||||
reporter: "html",
|
reporter: "html",
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
||||||
use: {
|
use: {
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
||||||
// baseURL: 'http://127.0.0.1:3000',
|
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
||||||
trace: "retain-on-failure",
|
trace: "retain-on-failure",
|
||||||
// Increase the timeout for navigations to give dx time to build the project
|
|
||||||
navigationTimeout: 50 * 60 * 1000,
|
navigationTimeout: 50 * 60 * 1000,
|
||||||
},
|
},
|
||||||
|
|
||||||
timeout: 50 * 60 * 1000,
|
timeout: 50 * 60 * 1000,
|
||||||
|
webServer: activeServers.map((s) => ({
|
||||||
/* Configure projects for major browsers */
|
command: s.command,
|
||||||
|
port: s.port,
|
||||||
|
cwd: s.cwd ? path.join(__dirname, s.cwd) : __dirname,
|
||||||
|
timeout: 50 * 60 * 1000,
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
env: s.env,
|
||||||
|
})),
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: "chromium",
|
name: "chromium",
|
||||||
grep,
|
grep: process.platform === "win32" ? /windows/ : undefined,
|
||||||
grepInvert,
|
grepInvert: process.platform !== "win32" ? /windows/ : undefined,
|
||||||
use: { ...devices["Desktop Chrome"] },
|
use: { ...devices["Desktop Chrome"] },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
|
||||||
webServer,
|
|
||||||
});
|
});
|
||||||
@@ -8,7 +8,7 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus = { path = "../../dioxus", features = ["web"] }
|
dioxus = { path = "../../dioxus", features = ["web"] }
|
||||||
wasm-bindgen = { version = "0.2.100" }
|
wasm-bindgen = { version = "0.2.114" }
|
||||||
web-sys = { version = "*", features = ["MouseEvent"] }
|
web-sys = { version = "*", features = ["MouseEvent"] }
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// @ts-check
|
||||||
|
const { test: base, expect } = require("@playwright/test");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const test = base.extend({
|
||||||
|
browser: async ({ playwright }, use) => {
|
||||||
|
const browser = await playwright.chromium.connectOverCDP("http://127.0.0.1:8788");
|
||||||
|
await use(browser);
|
||||||
|
await browser.close();
|
||||||
|
},
|
||||||
|
context: async ({ browser }, use) => {
|
||||||
|
const context = browser.contexts()[0];
|
||||||
|
await use(context);
|
||||||
|
},
|
||||||
|
page: async ({ context }, use) => {
|
||||||
|
const page = context.pages()[0];
|
||||||
|
await use(page);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hotPatchTimeout = { timeout: 1000 * 60 * 2 };
|
||||||
|
|
||||||
|
test("windows fullstack hotpatch", async ({ page }) => {
|
||||||
|
const mainPath = "windows-hotpatch-fullstack-temp/src/main.rs";
|
||||||
|
const stylePath = "windows-hotpatch-fullstack-temp/assets/style.css";
|
||||||
|
let mainContent = fs.readFileSync(mainPath, "utf8");
|
||||||
|
let styleContent = fs.readFileSync(stylePath, "utf8");
|
||||||
|
|
||||||
|
// Reset any changes from prior runs.
|
||||||
|
mainContent = mainContent.replace(/Ok\(\s*2\s*\)/g, "Ok(1)");
|
||||||
|
mainContent = mainContent.replace("Click button! Count:", "Click me! Count:");
|
||||||
|
fs.writeFileSync(mainPath, mainContent);
|
||||||
|
styleContent = styleContent.replace("background-color: blue;", "background-color: red;");
|
||||||
|
fs.writeFileSync(stylePath, styleContent);
|
||||||
|
|
||||||
|
// ** Verify the initial fat build is working **
|
||||||
|
const main = page.locator("#main");
|
||||||
|
await expect(main).toContainText("Click me! Count: 0");
|
||||||
|
|
||||||
|
// Click and verify the server function works.
|
||||||
|
const button = page.locator("button#increment-button");
|
||||||
|
await button.click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
await expect(main).toContainText("Click me! Count: 1");
|
||||||
|
|
||||||
|
// Verify initial CSS is applied.
|
||||||
|
await expect(main).toHaveCSS("background-color", "rgb(255, 0, 0)");
|
||||||
|
|
||||||
|
// ** Hot patch: change server fn return value and button text **
|
||||||
|
const updatedContent = mainContent
|
||||||
|
.replace(/Ok\(\s*1\s*\)/g, "Ok(2)")
|
||||||
|
.replace("Click me! Count:", "Click button! Count:");
|
||||||
|
fs.writeFileSync(mainPath, updatedContent);
|
||||||
|
|
||||||
|
// Wait for the hot patch to apply (text should update).
|
||||||
|
await expect(main).toContainText("Click button! Count: 1", hotPatchTimeout);
|
||||||
|
|
||||||
|
// Click and verify the new increment amount from the patched server fn.
|
||||||
|
await button.click();
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
await expect(main).toContainText("Click button! Count: 3");
|
||||||
|
|
||||||
|
// ** Hot patch CSS **
|
||||||
|
const updatedStyle = styleContent.replace(
|
||||||
|
"background-color: red;",
|
||||||
|
"background-color: blue;"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(stylePath, updatedStyle);
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Wait for the CSS hot patch to apply.
|
||||||
|
await expect(main).toHaveCSS(
|
||||||
|
"background-color",
|
||||||
|
"rgb(0, 0, 255)",
|
||||||
|
hotPatchTimeout
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "windows-hotpatch-fullstack"
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.0.1"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dioxus = { path = "../../dioxus", features = ["fullstack"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
desktop = ["dioxus/desktop"]
|
||||||
|
server = ["dioxus/server"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#main {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
const CSS: Asset = asset!("/assets/style.css");
|
||||||
|
|
||||||
|
fn app() -> Element {
|
||||||
|
let mut num = use_signal(|| 0);
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
document::Link {
|
||||||
|
href: CSS,
|
||||||
|
rel: "stylesheet",
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
id: "increment-button",
|
||||||
|
onclick: move |_| async move {
|
||||||
|
let increment_amount = get_count().await.unwrap();
|
||||||
|
*num.write() += increment_amount;
|
||||||
|
},
|
||||||
|
"Click me! Count: {num}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/api/get_count")]
|
||||||
|
async fn get_count() -> Result<i32> {
|
||||||
|
Ok(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let builder = dioxus::LaunchBuilder::new();
|
||||||
|
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
let builder = builder.with_cfg(
|
||||||
|
dioxus::desktop::Config::new().with_windows_browser_args("--remote-debugging-port=8788"),
|
||||||
|
);
|
||||||
|
|
||||||
|
builder.launch(app);
|
||||||
|
}
|
||||||
@@ -76,7 +76,7 @@ impl<T> Memo<T> {
|
|||||||
spawn_isomorphic(async move {
|
spawn_isomorphic(async move {
|
||||||
while rx.next().await.is_some() {
|
while rx.next().await.is_some() {
|
||||||
// Remove any pending updates
|
// Remove any pending updates
|
||||||
while rx.try_next().is_ok() {}
|
while rx.try_recv().is_ok() {}
|
||||||
memo.recompute();
|
memo.recompute();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ pub(crate) fn extend_store(args: ExtendArgs, mut input: ItemImpl) -> syn::Result
|
|||||||
let visibility = args
|
let visibility = args
|
||||||
.visibility
|
.visibility
|
||||||
.unwrap_or_else(|| syn::Visibility::Inherited);
|
.unwrap_or_else(|| syn::Visibility::Inherited);
|
||||||
if input.trait_.is_some() {
|
if let Some(input_trait) = input.trait_.as_ref() {
|
||||||
return Err(syn::Error::new_spanned(
|
return Err(syn::Error::new_spanned(
|
||||||
input.trait_.unwrap().1,
|
input_trait.1.clone(),
|
||||||
"The `store` attribute can only be used on `impl Store<T> { ... }` blocks, not trait implementations.",
|
"The `store` attribute can only be used on `impl Store<T> { ... }` blocks, not trait implementations.",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,8 @@ pub(crate) fn extend_store(args: ExtendArgs, mut input: ItemImpl) -> syn::Result
|
|||||||
let trait_definition = impl_to_trait_body(&extension_name, &input, &visibility)?;
|
let trait_definition = impl_to_trait_body(&extension_name, &input, &visibility)?;
|
||||||
|
|
||||||
// Reformat the type to be generic over the lens
|
// Reformat the type to be generic over the lens
|
||||||
input.self_ty = Box::new(parse_quote!(#store_path<#item, #lens_generic>));
|
input.self_ty = parse_quote!(#store_path<#item, #lens_generic>);
|
||||||
|
|
||||||
// Change the standalone impl block to a trait impl block
|
// Change the standalone impl block to a trait impl block
|
||||||
let (_, trait_generics, _) = input.generics.split_for_impl();
|
let (_, trait_generics, _) = input.generics.split_for_impl();
|
||||||
input.trait_ = Some((
|
input.trait_ = Some((
|
||||||
|
|||||||
Reference in new issue
Block a user