diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef207a6..fe0f7f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,6 +97,9 @@ jobs: run: cargo build --manifest-path app/src-tauri/Cargo.toml -p wisp-engine-sherpa - name: Build the app run: cargo build --manifest-path app/src-tauri/Cargo.toml + - name: Verify staged Windows runtime + shell: bash + run: ./scripts/verify-windows-runtime.sh app/src-tauri/windows-runtime # Proves the macOS app builds — including the macOS-ONLY code no other CI job ever compiles: # ScreenCaptureKit system audio, the WebRTC AEC3 echo canceller, Metal whisper.cpp, and Apple diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63c009d..55ebeb2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: # with whisper.cpp's automatic CPU fallback when there's no Vulkan device. sherpa stays CPU. - os: windows-latest label: Windows x64 - args: "--features whisper-vulkan" + args: "--features whisper-vulkan --config src-tauri/tauri.vulkan.conf.json" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -104,6 +104,13 @@ jobs: with: version: 1.4.309.0 cache: true + # The SDK supplies headers + the import library, but not the redistributable loader DLL. Stage + # LunarG's official, checksum-pinned x64 runtime so Wisp can launch and use the CPU fallback even + # on machines whose GPU driver did not install vulkan-1.dll. + - name: Stage the official Vulkan loader + if: runner.os == 'Windows' + shell: bash + run: ./scripts/stage-vulkan-loader.sh app/src-tauri/windows-runtime - name: Install frontend deps working-directory: app @@ -155,3 +162,15 @@ jobs: - name: Verify bundled macOS entitlements if: runner.os == 'macOS' run: ./scripts/verify-macos-entitlements.sh app/src-tauri/target/release/bundle/dmg/Wisp_*.dmg + + - name: Verify bundled Windows runtime + if: runner.os == 'Windows' + shell: bash + run: ./scripts/verify-windows-runtime.sh D:/b/release/bundle/nsis/Wisp_*-setup.exe --require-vulkan + + - name: Smoke-test the packaged Windows app + if: runner.os == 'Windows' + shell: pwsh + run: | + $installer = Get-ChildItem 'D:\b\release\bundle\nsis\Wisp_*-setup.exe' | Select-Object -First 1 + ./scripts/smoke-windows-installer.ps1 -Installer $installer.FullName diff --git a/.github/workflows/windows-vulkan.yml b/.github/workflows/windows-vulkan.yml index a5f346d..eeedf29 100644 --- a/.github/workflows/windows-vulkan.yml +++ b/.github/workflows/windows-vulkan.yml @@ -8,10 +8,17 @@ name: Windows Vulkan whisper smoke on: workflow_dispatch: {} - # Validate on pushes to the feature branch (a brand-new workflow can't be dispatched from a - # non-default branch). Drop this trigger once the path is proven and the workflow lands on main. - push: - branches: [feat/windows-vulkan-whisper] + pull_request: + branches: [main] + paths: + - ".github/workflows/windows-vulkan.yml" + - "app/src-tauri/build.rs" + - "app/src-tauri/Cargo.toml" + - "app/src-tauri/tauri*.json" + - "crates/wisp-engine-whisper-cpp/**" + - "scripts/smoke-windows-installer.ps1" + - "scripts/stage-vulkan-loader.sh" + - "scripts/verify-windows-runtime.sh" env: CARGO_TERM_COLOR: always @@ -44,6 +51,9 @@ jobs: with: version: 1.4.309.0 cache: true + - name: Stage the official Vulkan loader + shell: bash + run: ./scripts/stage-vulkan-loader.sh app/src-tauri/windows-runtime - name: Install the frontend deps working-directory: app run: npm ci @@ -56,7 +66,7 @@ jobs: shell: pwsh run: | Write-Host "VULKAN_SDK = $env:VULKAN_SDK" - npm run tauri build -- --features whisper-vulkan + npm run tauri build -- --features whisper-vulkan --config src-tauri/tauri.vulkan.conf.json # Show where the NSIS bundler dropped the installer so the release.yml recipe can target the same # path (confirms CARGO_TARGET_DIR is honoured by the bundler, not just the compiler). - name: Locate the installer (diagnostic) @@ -65,6 +75,14 @@ jobs: run: | Get-ChildItem -Recurse "D:\b","app\src-tauri\target" -Include *-setup.exe -ErrorAction SilentlyContinue | Select-Object FullName, Length | Format-Table -Auto + - name: Verify the installer runtime + shell: bash + run: ./scripts/verify-windows-runtime.sh D:/b/release/bundle/nsis/Wisp_*-setup.exe --require-vulkan + - name: Smoke-test the packaged app + shell: pwsh + run: | + $installer = Get-ChildItem 'D:\b\release\bundle\nsis\Wisp_*-setup.exe' | Select-Object -First 1 + ./scripts/smoke-windows-installer.ps1 -Installer $installer.FullName # Upload the installer so it can be downloaded and tested on real Windows hardware (GPU + non-GPU). - name: Upload the Windows GPU installer if: always() diff --git a/app/src-tauri/tauri.vulkan.conf.json b/app/src-tauri/tauri.vulkan.conf.json new file mode 100644 index 0000000..d845b6b --- /dev/null +++ b/app/src-tauri/tauri.vulkan.conf.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "bundle": { + "resources": { + "windows-runtime/vulkan-1.dll": "vulkan-1.dll", + "windows-runtime/VulkanRT-License.txt": "VulkanRT-License.txt" + } + } +} diff --git a/scripts/smoke-windows-installer.ps1 b/scripts/smoke-windows-installer.ps1 new file mode 100644 index 0000000..13aeeca --- /dev/null +++ b/scripts/smoke-windows-installer.ps1 @@ -0,0 +1,38 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Installer +) + +$ErrorActionPreference = "Stop" + +if (-not (Test-Path -LiteralPath $Installer -PathType Leaf)) { + throw "Windows installer not found: $Installer" +} + +$extractDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "wisp-windows-smoke-$([guid]::NewGuid())" +$process = $null + +try { + New-Item -ItemType Directory -Path $extractDirectory | Out-Null + & 7z x -y "-o$extractDirectory" $Installer | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "7z failed to extract $Installer" + } + + $executable = Join-Path $extractDirectory "Wisp.exe" + $process = Start-Process -FilePath $executable -WorkingDirectory $extractDirectory -PassThru + if ($process.WaitForExit(15000)) { + throw "Packaged Wisp.exe exited during startup with code $($process.ExitCode)" + } + + Write-Host "Packaged Wisp.exe remained running for 15 seconds" +} +finally { + if ($null -ne $process -and -not $process.HasExited) { + Stop-Process -Id $process.Id -Force + $process.WaitForExit() + } + if (Test-Path -LiteralPath $extractDirectory) { + Remove-Item -LiteralPath $extractDirectory -Recurse -Force + } +} diff --git a/scripts/stage-vulkan-loader.sh b/scripts/stage-vulkan-loader.sh new file mode 100755 index 0000000..0dc3bc2 --- /dev/null +++ b/scripts/stage-vulkan-loader.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +# Official LunarG runtime components matching the Vulkan SDK used by the Windows workflows. +# The component archive is intended for redistribution and includes the runtime license. +version=1.4.309.0 +archive_name="VulkanRT-${version}-Components.zip" +archive_sha256=7d969f4d7b44e387667d3148f61559497c22d50cbe3d50adc9e5409afbce2df1 +archive_url="https://sdk.lunarg.com/sdk/download/${version}/windows/${archive_name}" +destination=$1 +work_dir=$(mktemp -d "${TMPDIR:-/tmp}/wisp-vulkan-runtime.XXXXXX") +archive="$work_dir/$archive_name" + +cleanup() { + if [[ -d "$work_dir" ]]; then + rm -rf "$work_dir" + fi +} +trap cleanup EXIT + +command -v curl >/dev/null 2>&1 || { + echo "curl is required to download the Vulkan runtime" >&2 + exit 1 +} +command -v 7z >/dev/null 2>&1 || { + echo "7z is required to extract the Vulkan runtime" >&2 + exit 1 +} + +curl --fail --location --retry 3 --output "$archive" "$archive_url" +if command -v shasum >/dev/null 2>&1; then + actual_sha256=$(shasum -a 256 "$archive" | cut -d ' ' -f 1) +else + actual_sha256=$(sha256sum "$archive" | cut -d ' ' -f 1) +fi +[[ $actual_sha256 == "$archive_sha256" ]] || { + echo "Vulkan runtime checksum mismatch: expected $archive_sha256, got $actual_sha256" >&2 + exit 1 +} + +mkdir -p "$destination" +7z e -y "-o$destination" "$archive" "*/x64/vulkan-1.dll" "*/VulkanRT-License.txt" >/dev/null + +for file in vulkan-1.dll VulkanRT-License.txt; do + [[ -s "$destination/$file" ]] || { + echo "Failed to stage $file from the Vulkan runtime archive" >&2 + exit 1 + } +done + +echo "Staged LunarG Vulkan runtime $version in $destination" diff --git a/scripts/verify-windows-runtime.sh b/scripts/verify-windows-runtime.sh new file mode 100755 index 0000000..07c4513 --- /dev/null +++ b/scripts/verify-windows-runtime.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $0 [--require-vulkan]" >&2 + exit 2 +} + +[[ $# -ge 1 && $# -le 2 ]] || usage +input=$1 +require_vulkan=false +if [[ $# -eq 2 ]]; then + [[ $2 == "--require-vulkan" ]] || usage + require_vulkan=true +fi +extract_dir= + +cleanup() { + if [[ -n "$extract_dir" && -d "$extract_dir" ]]; then + rm -rf "$extract_dir" + fi +} +trap cleanup EXIT + +if [[ -f "$input" ]]; then + command -v 7z >/dev/null 2>&1 || { + echo "7z is required to inspect the NSIS installer" >&2 + exit 1 + } + extract_dir=$(mktemp -d "${TMPDIR:-/tmp}/wisp-windows-package.XXXXXX") + 7z x -y "-o$extract_dir" "$input" >/dev/null + runtime_dir=$extract_dir +elif [[ -d "$input" ]]; then + runtime_dir=$input +else + echo "Runtime directory or installer not found: $input" >&2 + exit 1 +fi + +files=( + "onnxruntime.dll:1000000" + "onnxruntime_providers_shared.dll:1000" + "sherpa-onnx-c-api.dll:1000000" +) + +if [[ $require_vulkan == true ]]; then + files+=( + "vulkan-1.dll:100000" + "VulkanRT-License.txt:1000" + ) +fi + +if [[ -n "$extract_dir" ]]; then + files+=( + "Wisp.exe:1000000" + "resources/silero_vad.onnx:100000" + ) +fi + +for expected in "${files[@]}"; do + name=${expected%%:*} + minimum=${expected##*:} + file="$runtime_dir/$name" + if [[ ! -f "$file" ]]; then + echo "Missing Windows package file: $name" >&2 + exit 1 + fi + size=$(wc -c < "$file" | tr -d '[:space:]') + if (( size < minimum )); then + echo "Windows package file is truncated: $name is $size bytes (minimum $minimum)" >&2 + exit 1 + fi + echo "Verified $name ($size bytes)" +done