diff --git a/.github/workflows/desktop-pr-build.yml b/.github/workflows/desktop-pr-build.yml index dfb1d31c..494678bd 100644 --- a/.github/workflows/desktop-pr-build.yml +++ b/.github/workflows/desktop-pr-build.yml @@ -138,7 +138,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # was stable with: - toolchain: 1.95.0 + toolchain: 1.88.0 - name: Install sccache shell: bash @@ -170,61 +170,9 @@ jobs: ${{ runner.os }}-sccache-windows- ${{ runner.os }}-sccache- - - name: Provide ONNX Runtime (Windows) - shell: bash - run: | - ./frontend/src-tauri/scripts/provide-windows-onnxruntime.sh >> "$GITHUB_ENV" - - - name: Stage Windows runtime DLLs for bundling - shell: pwsh - run: | - # maple.exe links onnxruntime.dll by ordinal; without these next to the - # exe the loader binds to the OS Windows-ML onnxruntime.dll (v1.17) and - # TTS hangs at Session::builder. See resources/windows/README.md. - $dest = "frontend/src-tauri/resources/windows" - New-Item -ItemType Directory -Force -Path $dest | Out-Null - # ONNX Runtime 1.22.0 (already downloaded + SHA-verified; path in env) - Copy-Item "$env:ORT_DYLIB_PATH" (Join-Path $dest "onnxruntime.dll") -Force - # MSVC C++ runtime DLLs onnxruntime.dll depends on. Find a source dir - # holding all four, independent of the runner's VS year/edition: prefer - # the versioned redist (located via vswhere), fall back to System32. - $crtDlls = 'VCRUNTIME140.dll','VCRUNTIME140_1.dll','MSVCP140.dll','MSVCP140_1.dll' - $candidates = @() - $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - if (Test-Path $vswhere) { - $vs = & $vswhere -latest -products * -property installationPath - if ($vs) { - $candidates += Get-ChildItem (Join-Path $vs 'VC\Redist\MSVC\*\x64') -Directory -ErrorAction SilentlyContinue | - Where-Object { $_.Name -match '^Microsoft\.VC\d+\.CRT$' } | ForEach-Object FullName - } - } - $candidates += "$env:WINDIR\System32" - $src = $candidates | Where-Object { $d = $_; -not ($crtDlls | Where-Object { -not (Test-Path (Join-Path $d $_)) }) } | Select-Object -First 1 - if (-not $src) { throw "No directory has all CRT DLLs. Searched: $($candidates -join '; ')" } - Write-Host "CRT source: $src" - foreach ($dll in $crtDlls) { Copy-Item (Join-Path $src $dll) (Join-Path $dest $dll) -Force } - Get-ChildItem $dest | Select-Object Name, Length - - - name: Install frontend dependencies - working-directory: ./frontend - run: bun install --frozen-lockfile --ignore-scripts - - - name: Configure sccache - shell: bash - run: | - { - echo "RUSTC_WRAPPER=sccache" - echo "SCCACHE_CACHE_SIZE=2G" - } >> "$GITHUB_ENV" - - name: Build Tauri App (Windows, unsigned) - working-directory: ./frontend shell: bash - run: bun tauri build --no-sign --config '{"bundle":{"createUpdaterArtifacts":false}}' - env: - VITE_OPEN_SECRET_API_URL: https://enclave.secretgpt.ai - VITE_MAPLE_BILLING_API_URL: https://billing-dev.opensecret.cloud - VITE_CLIENT_ID: ba5a14b5-d915-47b1-b7b1-afda52bc5fc6 + run: ./scripts/ci/desktop-windows-pr.sh - name: Show sccache stats run: sccache --show-stats @@ -235,4 +183,30 @@ jobs: name: maple-windows-x64-pr path: | frontend/src-tauri/target/release/bundle/nsis/*.exe + frontend/src-tauri/target/release/maple.exe + frontend/src-tauri/resources/windows/*.dll + frontend/src-tauri/target/reproducibility/desktop-pr-windows-*.sha256 retention-days: 5 + + verify-windows-artifacts: + needs: + - build-windows + runs-on: ubuntu-latest-8-cores + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # was v4 + with: + persist-credentials: false + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # was v22 + with: + github-token: "" + + - name: Download Windows artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # was v4 + with: + name: maple-windows-x64-pr + path: artifacts + + - name: Verify Windows artifact reproducibility proofs + run: nix develop .#ci -c ./scripts/ci/verify-release-artifacts.sh artifacts windows diff --git a/frontend/src-tauri/resources/windows/README.md b/frontend/src-tauri/resources/windows/README.md index 9147f34a..f7aa8c29 100644 --- a/frontend/src-tauri/resources/windows/README.md +++ b/frontend/src-tauri/resources/windows/README.md @@ -45,39 +45,29 @@ must be staged here **before `bun tauri build`** on Windows. ## CI staging -The Windows CI workflows (`desktop-pr-build.yml`, `desktop-build.yml`) stage -these automatically in the **"Stage Windows runtime DLLs for bundling"** step, -which runs after "Provide ONNX Runtime (Windows)" and before the Tauri build. +The Windows PR workflow stages these automatically through +`scripts/ci/desktop-windows-pr.sh` before the Tauri build. That script uses: + +- SHA-verified ONNX Runtime from `scripts/provide-windows-onnxruntime.sh`. +- A SHA-verified, versioned Microsoft `VC_redist.x64.exe` URL pinned in + `frontend/src-tauri/scripts/onnxruntime-pins.sh`. +- A SHA-verified WiX CLI NuGet package, used only to extract the VC++ redist + bootstrapper payload reproducibly. + +The build emits `target/reproducibility/desktop-pr-windows-*.sha256` proof +manifests, and CI verifies those manifests from uploaded artifacts. For a **local** Windows build, run `scripts/provide-windows-onnxruntime.sh` -first (it exports `ORT_DYLIB_PATH`), then stage the same five files: - -1. **`onnxruntime.dll`** — from the SHA-verified ONNX Runtime download: - - ```bash - cp "$ORT_DYLIB_PATH" frontend/src-tauri/resources/windows/onnxruntime.dll - ``` - -2. **The 4 MSVC CRT DLLs** — from the Visual Studio redist on the machine, - located via `vswhere` so it's independent of the VS year/edition (the - `^Microsoft\.VC\d+\.CRT$` filter avoids the neighbouring `DebugCRT`/`OPENMP` - folders); falls back to `System32`: - - ```powershell - $crtDlls = 'VCRUNTIME140.dll','VCRUNTIME140_1.dll','MSVCP140.dll','MSVCP140_1.dll' - $candidates = @() - $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - if (Test-Path $vswhere) { - $vs = & $vswhere -latest -products * -property installationPath - if ($vs) { - $candidates += Get-ChildItem (Join-Path $vs 'VC\Redist\MSVC\*\x64') -Directory -ErrorAction SilentlyContinue | - Where-Object { $_.Name -match '^Microsoft\.VC\d+\.CRT$' } | ForEach-Object FullName - } - } - $candidates += "$env:WINDIR\System32" - $src = $candidates | Where-Object { $d = $_; -not ($crtDlls | Where-Object { -not (Test-Path (Join-Path $d $_)) }) } | Select-Object -First 1 - $crtDlls | ForEach-Object { Copy-Item (Join-Path $src $_) frontend\src-tauri\resources\windows\ } - ``` +first (it exports `ORT_DYLIB_PATH`), then run the same staging helper: + +```powershell +$env:MAPLE_WINDOWS_VC_REDIST_VERSION = "14.44.35211" +$env:MAPLE_WINDOWS_VC_REDIST_URL = "" +$env:MAPLE_WINDOWS_VC_REDIST_SHA256 = "" +.\frontend\src-tauri\scripts\stage-windows-runtime-dlls.ps1 ` + -OrtDllPath "$env:ORT_DYLIB_PATH" ` + -Destination .\frontend\src-tauri\resources\windows +``` The hook reads these files at makensis compile time, so they must be present here before the build runs (the CI staging step guarantees that); a missing diff --git a/frontend/src-tauri/scripts/onnxruntime-pins.sh b/frontend/src-tauri/scripts/onnxruntime-pins.sh index 176981e1..abbf6fb3 100644 --- a/frontend/src-tauri/scripts/onnxruntime-pins.sh +++ b/frontend/src-tauri/scripts/onnxruntime-pins.sh @@ -72,6 +72,62 @@ onnxruntime_windows_x64_dll_sha256_for_version() { esac } +windows_vc_redist_x64_version() { + printf '%s\n' "14.44.35211" +} + +windows_vc_redist_x64_url_for_version() { + case "$1" in + 14.44.35211) + printf '%s\n' "https://download.visualstudio.microsoft.com/download/pr/7ebf5fdb-36dc-4145-b0a0-90d3d5990a61/CC0FF0EB1DC3F5188AE6300FAEF32BF5BEEBA4BDD6E8E445A9184072096B713B/VC_redist.x64.exe" + ;; + *) + echo "No pinned Windows x64 VC++ Redistributable URL for version '$1'." >&2 + return 1 + ;; + esac +} + +windows_vc_redist_x64_archive_sha256_for_version() { + case "$1" in + 14.44.35211) + printf '%s\n' "cc0ff0eb1dc3f5188ae6300faef32bf5beeba4bdd6e8e445a9184072096b713b" + ;; + *) + echo "No pinned Windows x64 VC++ Redistributable SHA-256 for version '$1'." >&2 + return 1 + ;; + esac +} + +windows_wix_cli_version() { + printf '%s\n' "6.0.2" +} + +windows_wix_cli_url_for_version() { + case "$1" in + 6.0.2) + printf '%s\n' "https://www.nuget.org/api/v2/package/wix/6.0.2" + ;; + *) + echo "No pinned WiX CLI NuGet package URL for version '$1'." >&2 + return 1 + ;; + esac +} + +windows_wix_cli_archive_sha256_for_version() { + case "$1" in + 6.0.2) + printf '%s\n' "13caed0aa86898c9952eb8ba82c6ac6b43d1575bb731ac848e5edf5490a10428" + ;; + *) + echo "No pinned WiX CLI NuGet package SHA-256 for version '$1'." >&2 + return 1 + ;; + esac +} + onnxruntime_ios_commit_for_version() { case "$1" in 1.22.2) diff --git a/frontend/src-tauri/scripts/stage-windows-runtime-dlls.ps1 b/frontend/src-tauri/scripts/stage-windows-runtime-dlls.ps1 new file mode 100644 index 00000000..f1e018eb --- /dev/null +++ b/frontend/src-tauri/scripts/stage-windows-runtime-dlls.ps1 @@ -0,0 +1,373 @@ +param( + [Parameter(Mandatory = $true)] + [string] $OrtDllPath, + + [Parameter(Mandatory = $true)] + [string] $Destination +) + +$ErrorActionPreference = "Stop" + +$dllNames = @( + "VCRUNTIME140.dll", + "VCRUNTIME140_1.dll", + "MSVCP140.dll", + "MSVCP140_1.dll" +) + +function Require-Env { + param([Parameter(Mandatory = $true)][string] $Name) + + $value = [Environment]::GetEnvironmentVariable($Name) + if ([string]::IsNullOrWhiteSpace($value)) { + throw "Missing required environment variable: $Name" + } + + return $value +} + +function Get-Sha256 { + param([Parameter(Mandatory = $true)][string] $Path) + + return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() +} + +function Assert-Sha256 { + param( + [Parameter(Mandatory = $true)][string] $Label, + [Parameter(Mandatory = $true)][string] $Path, + [Parameter(Mandatory = $true)][string] $Expected + ) + + $actual = Get-Sha256 -Path $Path + if ($actual -ne $Expected.ToLowerInvariant()) { + throw "$Label SHA-256 mismatch for ${Path}. expected=$Expected actual=$actual" + } +} + +function Invoke-Download { + param( + [Parameter(Mandatory = $true)][string] $Url, + [Parameter(Mandatory = $true)][string] $OutFile + ) + + for ($attempt = 1; $attempt -le 5; $attempt++) { + try { + Invoke-WebRequest -Uri $Url -OutFile $OutFile + return + } catch { + if ($attempt -eq 5) { + throw + } + Start-Sleep -Seconds (2 * $attempt) + } + } +} + +function Invoke-CheckedProcess { + param( + [Parameter(Mandatory = $true)][string] $FilePath, + [Parameter(Mandatory = $true)][string] $Arguments, + [int[]] $AllowedExitCodes = @(0, 3010) + ) + + $process = Start-Process -FilePath $FilePath -ArgumentList $Arguments -NoNewWindow -Wait -PassThru + if ($AllowedExitCodes -notcontains $process.ExitCode) { + throw "Command failed with exit code $($process.ExitCode): $FilePath $Arguments" + } +} + +function Get-PayloadKind { + param([Parameter(Mandatory = $true)][string] $Path) + + $bytes = New-Object byte[] 8 + $stream = [IO.File]::OpenRead($Path) + try { + $read = $stream.Read($bytes, 0, $bytes.Length) + } finally { + $stream.Dispose() + } + + if (($read -ge 4) -and ([Text.Encoding]::ASCII.GetString($bytes, 0, 4) -eq "MSCF")) { + return "cab" + } + + if ( + ($read -ge 8) -and + ($bytes[0] -eq 0xd0) -and + ($bytes[1] -eq 0xcf) -and + ($bytes[2] -eq 0x11) -and + ($bytes[3] -eq 0xe0) -and + ($bytes[4] -eq 0xa1) -and + ($bytes[5] -eq 0xb1) -and + ($bytes[6] -eq 0x1a) -and + ($bytes[7] -eq 0xe1) + ) { + return "msi" + } + + if (($read -ge 2) -and ($bytes[0] -eq 0x4d) -and ($bytes[1] -eq 0x5a)) { + return "exe" + } + + return "unknown" +} + +function Copy-PayloadWithExtension { + param( + [Parameter(Mandatory = $true)][string] $Path, + [Parameter(Mandatory = $true)][string] $Extension, + [Parameter(Mandatory = $true)][string] $OutDir, + [Parameter(Mandatory = $true)][int] $Index + ) + + if ([IO.Path]::GetExtension($Path) -ieq $Extension) { + return $Path + } + + $target = Join-Path $OutDir ("payload-{0}{1}" -f $Index, $Extension) + Copy-Item -LiteralPath $Path -Destination $target -Force + return $target +} + +function Test-PortableExecutableMachineAmd64 { + param([Parameter(Mandatory = $true)][string] $Path) + + $stream = $null + $reader = $null + try { + $stream = [IO.File]::OpenRead($Path) + if ($stream.Length -lt 64) { + return $false + } + + $reader = New-Object IO.BinaryReader($stream) + if ($reader.ReadUInt16() -ne 0x5A4D) { + return $false + } + + $stream.Position = 0x3C + $peOffset = $reader.ReadInt32() + if (($peOffset -lt 0) -or ($stream.Length -lt ($peOffset + 6))) { + return $false + } + + $stream.Position = $peOffset + if ($reader.ReadUInt32() -ne 0x00004550) { + return $false + } + + return $reader.ReadUInt16() -eq 0x8664 + } catch { + return $false + } finally { + if ($null -ne $reader) { + $reader.Dispose() + } elseif ($null -ne $stream) { + $stream.Dispose() + } + } +} + +function Expand-VcRedist { + param( + [Parameter(Mandatory = $true)][string] $ExePath, + [Parameter(Mandatory = $true)][string] $WixExe, + [Parameter(Mandatory = $true)][string] $OutDir + ) + + $burnDir = Join-Path $OutDir "burn" + $layoutDir = Join-Path $OutDir "layout" + $adminDir = Join-Path $OutDir "admin" + $cabDir = Join-Path $OutDir "cab" + $payloadDir = Join-Path $OutDir "payloads" + + Remove-Item -LiteralPath $burnDir, $layoutDir, $adminDir, $cabDir, $payloadDir -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Force -Path $burnDir, $layoutDir, $adminDir, $cabDir, $payloadDir | Out-Null + + Invoke-CheckedProcess ` + -FilePath $WixExe ` + -Arguments "burn extract `"$ExePath`" -o `"$burnDir`"" + + Invoke-CheckedProcess ` + -FilePath $ExePath ` + -Arguments "/layout `"$layoutDir`" /quiet /norestart" + + $packageRoots = @($burnDir, $layoutDir) + + $payloads = @(Get-ChildItem -LiteralPath $packageRoots -Recurse -File | + Sort-Object FullName | + ForEach-Object { + $kind = Get-PayloadKind -Path $_.FullName + Write-Host ("vc-redist-payload {0} {1} len={2}" -f $kind, $_.FullName, $_.Length) + [PSCustomObject]@{ + File = $_ + Kind = $kind + } + }) + + $cabIndex = 0 + foreach ($payload in ($payloads | Where-Object { $_.Kind -eq "cab" })) { + $cabIndex += 1 + $cabPath = Copy-PayloadWithExtension ` + -Path $payload.File.FullName ` + -Extension ".cab" ` + -OutDir $payloadDir ` + -Index $cabIndex + $targetDir = Join-Path $cabDir ("payload-{0}" -f $cabIndex) + New-Item -ItemType Directory -Force -Path $targetDir | Out-Null + & "$env:WINDIR\System32\expand.exe" -F:* $cabPath $targetDir | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Failed to expand $cabPath" + } + } + + $msiIndex = 0 + foreach ($payload in ($payloads | Where-Object { $_.Kind -eq "msi" })) { + $msiIndex += 1 + $msiPath = Copy-PayloadWithExtension ` + -Path $payload.File.FullName ` + -Extension ".msi" ` + -OutDir $payloadDir ` + -Index $msiIndex + $targetDir = Join-Path $adminDir ("payload-{0}" -f $msiIndex) + New-Item -ItemType Directory -Force -Path $targetDir | Out-Null + try { + Invoke-CheckedProcess ` + -FilePath "msiexec.exe" ` + -Arguments "/a `"$msiPath`" TARGETDIR=`"$targetDir`" /qn /norestart" + } catch { + Write-Host ("vc-redist-msi-admin-extract-skipped {0} {1}" -f $msiPath, $_.Exception.Message) + } + } + + return @($burnDir, $layoutDir, $adminDir, $cabDir) +} + +function Find-RequiredDll { + param( + [Parameter(Mandatory = $true)][string] $Name, + [Parameter(Mandatory = $true)][string[]] $Roots + ) + + $files = @() + foreach ($root in $Roots) { + $files += @(Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue) + } + + $x64Files = @($files | Where-Object { Test-PortableExecutableMachineAmd64 -Path $_.FullName }) + + $match = $x64Files | + Where-Object { $_.Name -ieq $Name } | + Sort-Object FullName | + Select-Object -First 1 + + if ($null -ne $match) { + return $match.FullName + } + + foreach ($file in ($x64Files | Sort-Object FullName)) { + $versionInfo = $null + try { + $versionInfo = $file.VersionInfo + } catch { + continue + } + + if ($null -eq $versionInfo) { + continue + } + + if ( + ($versionInfo.OriginalFilename -ieq $Name) -or + ($versionInfo.InternalName -ieq $Name) -or + ($versionInfo.FileDescription -ieq $Name) + ) { + Write-Host ("resolved-windows-runtime-dll {0} {1}" -f $Name, $file.FullName) + return $file.FullName + } + } + + Write-Host "available-vc-redist-payload-files:" + $files | + Sort-Object FullName | + Select-Object -First 200 | + ForEach-Object { + $originalFilename = "" + $internalName = "" + try { + $originalFilename = $_.VersionInfo.OriginalFilename + $internalName = $_.VersionInfo.InternalName + } catch { + } + Write-Host (" {0} len={1} original={2} internal={3}" -f $_.FullName, $_.Length, $originalFilename, $internalName) + } + + throw "Could not find $Name in extracted VC++ Redistributable payload roots: $($Roots -join '; ')" +} + +function Get-WixExe { + param([Parameter(Mandatory = $true)][string] $CacheRoot) + + $version = Require-Env -Name "MAPLE_WINDOWS_WIX_CLI_VERSION" + $url = Require-Env -Name "MAPLE_WINDOWS_WIX_CLI_URL" + $sha256 = Require-Env -Name "MAPLE_WINDOWS_WIX_CLI_SHA256" + $wixDir = Join-Path $CacheRoot "wix-$version" + $wixPackage = Join-Path $wixDir "wix.$version.nupkg" + $wixExtracted = Join-Path $wixDir "pkg" + $wixExe = Join-Path $wixExtracted "tools\net6.0\any\wix.exe" + + New-Item -ItemType Directory -Force -Path $wixDir | Out-Null + + if (!(Test-Path -LiteralPath $wixPackage)) { + Invoke-Download -Url $url -OutFile $wixPackage + } + Assert-Sha256 -Label "WiX CLI $version NuGet package" -Path $wixPackage -Expected $sha256 + + if (!(Test-Path -LiteralPath $wixExe)) { + Remove-Item -LiteralPath $wixExtracted -Recurse -Force -ErrorAction SilentlyContinue + Expand-Archive -LiteralPath $wixPackage -DestinationPath $wixExtracted -Force + } + + if (!(Test-Path -LiteralPath $wixExe)) { + throw "WiX executable was not found after extracting $wixPackage" + } + + return $wixExe +} + +$vcRedistVersion = Require-Env -Name "MAPLE_WINDOWS_VC_REDIST_VERSION" +$vcRedistUrl = Require-Env -Name "MAPLE_WINDOWS_VC_REDIST_URL" +$vcRedistSha256 = Require-Env -Name "MAPLE_WINDOWS_VC_REDIST_SHA256" + +$tauriDir = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$cacheDir = Join-Path $tauriDir "target\windows-runtime" +$redistDir = Join-Path $cacheDir "vc-redist-$vcRedistVersion" +$redistExe = Join-Path $redistDir "VC_redist.x64.exe" + +New-Item -ItemType Directory -Force -Path $Destination, $redistDir | Out-Null +Get-ChildItem -LiteralPath $Destination -Filter "*.dll" -File -ErrorAction SilentlyContinue | + Remove-Item -Force + +if (!(Test-Path -LiteralPath $redistExe)) { + Invoke-Download -Url $vcRedistUrl -OutFile $redistExe +} + +Assert-Sha256 -Label "VC++ Redistributable $vcRedistVersion" -Path $redistExe -Expected $vcRedistSha256 + +$wixExe = Get-WixExe -CacheRoot $cacheDir +$payloadRoots = Expand-VcRedist -ExePath $redistExe -WixExe $wixExe -OutDir $redistDir + +Copy-Item -LiteralPath $OrtDllPath -Destination (Join-Path $Destination "onnxruntime.dll") -Force + +foreach ($dllName in $dllNames) { + $source = Find-RequiredDll -Name $dllName -Roots $payloadRoots + Copy-Item -LiteralPath $source -Destination (Join-Path $Destination $dllName) -Force +} + +Get-ChildItem -LiteralPath $Destination -Filter "*.dll" -File | + Sort-Object Name | + ForEach-Object { + $hash = Get-Sha256 -Path $_.FullName + Write-Host ("sha256-windows-runtime-dll {0} {1}" -f $hash, $_.Name) + } diff --git a/scripts/ci/_common.sh b/scripts/ci/_common.sh index b99ff0f0..1b6e44d7 100755 --- a/scripts/ci/_common.sh +++ b/scripts/ci/_common.sh @@ -102,18 +102,28 @@ configure_sccache() { if command -v sccache >/dev/null 2>&1; then local os socket_root os="$(host_os)" - socket_root="${TMPDIR:-/tmp}" export RUSTC_WRAPPER="${RUSTC_WRAPPER:-sccache}" export SCCACHE_CACHE_SIZE="${SCCACHE_CACHE_SIZE:-2G}" - export SCCACHE_SERVER_UDS="${SCCACHE_SERVER_UDS:-${socket_root%/}/maple-sccache-${os}.sock}" export CARGO_CACHE_RUSTC_INFO="${CARGO_CACHE_RUSTC_INFO:-0}" case "${os}" in darwin) + socket_root="${TMPDIR:-/tmp}" + export SCCACHE_SERVER_UDS="${SCCACHE_SERVER_UDS:-${socket_root%/}/maple-sccache-${os}.sock}" export SCCACHE_DIR="${SCCACHE_DIR:-${HOME}/Library/Caches/Mozilla.sccache}" ;; + mingw* | msys* | cygwin*) + unset SCCACHE_SERVER_UDS + if [ -n "${LOCALAPPDATA:-}" ]; then + export SCCACHE_DIR="${SCCACHE_DIR:-${LOCALAPPDATA}\\Mozilla\\sccache}" + else + export SCCACHE_DIR="${SCCACHE_DIR:-${HOME}/AppData/Local/Mozilla/sccache}" + fi + ;; *) + socket_root="${TMPDIR:-/tmp}" + export SCCACHE_SERVER_UDS="${SCCACHE_SERVER_UDS:-${socket_root%/}/maple-sccache-${os}.sock}" export SCCACHE_DIR="${SCCACHE_DIR:-${HOME}/.cache/sccache}" ;; esac diff --git a/scripts/ci/desktop-windows-pr.sh b/scripts/ci/desktop-windows-pr.sh new file mode 100755 index 00000000..fe8cae74 --- /dev/null +++ b/scripts/ci/desktop-windows-pr.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" + +case "$(host_os)" in + mingw* | msys* | cygwin*) + ;; + *) + echo "desktop-windows-pr.sh must run on Windows under Git Bash/MSYS." >&2 + exit 1 + ;; +esac + +to_windows_path() { + if command -v cygpath >/dev/null 2>&1; then + cygpath -w "$1" + else + printf '%s\n' "$1" + fi +} + +prepare_windows_onnxruntime() { + local ort_env key value + + ort_env="$("${TAURI_DIR}/scripts/provide-windows-onnxruntime.sh")" + printf '%s\n' "${ort_env}" + + while IFS='=' read -r key value; do + case "${key}" in + ORT_LIB_LOCATION | ORT_SKIP_DOWNLOAD | ORT_DYLIB_PATH) + export "${key}=${value}" + ;; + esac + done <<< "${ort_env}" +} + +stage_windows_runtime_dlls() { + local vc_redist_version wix_cli_version + + vc_redist_version="$(windows_vc_redist_x64_version)" + export MAPLE_WINDOWS_VC_REDIST_VERSION="${vc_redist_version}" + export MAPLE_WINDOWS_VC_REDIST_URL + export MAPLE_WINDOWS_VC_REDIST_SHA256 + MAPLE_WINDOWS_VC_REDIST_URL="$(windows_vc_redist_x64_url_for_version "${vc_redist_version}")" + MAPLE_WINDOWS_VC_REDIST_SHA256="$(windows_vc_redist_x64_archive_sha256_for_version "${vc_redist_version}")" + + wix_cli_version="$(windows_wix_cli_version)" + export MAPLE_WINDOWS_WIX_CLI_VERSION="${wix_cli_version}" + export MAPLE_WINDOWS_WIX_CLI_URL + export MAPLE_WINDOWS_WIX_CLI_SHA256 + MAPLE_WINDOWS_WIX_CLI_URL="$(windows_wix_cli_url_for_version "${wix_cli_version}")" + MAPLE_WINDOWS_WIX_CLI_SHA256="$(windows_wix_cli_archive_sha256_for_version "${wix_cli_version}")" + + pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass \ + -File "$(to_windows_path "${TAURI_DIR}/scripts/stage-windows-runtime-dlls.ps1")" \ + -OrtDllPath "$(to_windows_path "${ORT_DYLIB_PATH:?ORT_DYLIB_PATH is required}")" \ + -Destination "$(to_windows_path "${TAURI_DIR}/resources/windows")" +} + +print_source_provenance + +install_frontend_deps +configure_sccache +use_pr_environment +configure_reproducible_build_metadata +prepare_windows_onnxruntime +stage_windows_runtime_dlls +build_frontend_dist + +cd "${FRONTEND_DIR}" + +remove_build_tree "${TAURI_DIR}/target/release/bundle/nsis" +bun tauri build --verbose --no-sign --config '{"build":{"beforeBuildCommand":null},"bundle":{"createUpdaterArtifacts":false}}' + +repro_dir="${TAURI_DIR}/target/reproducibility" +mkdir -p "${repro_dir}" + +windows_artifacts=() +while IFS= read -r -d '' file; do + windows_artifacts+=("${file}") +done < <(find "${TAURI_DIR}/target/release/bundle/nsis" -type f -name '*.exe' -print0 | LC_ALL=C sort -z) + +if [ -f "${TAURI_DIR}/target/release/maple.exe" ]; then + windows_artifacts+=("${TAURI_DIR}/target/release/maple.exe") +fi + +windows_runtime_dlls=() +while IFS= read -r -d '' file; do + windows_runtime_dlls+=("${file}") +done < <(find "${TAURI_DIR}/resources/windows" -type f -name '*.dll' -print0 | LC_ALL=C sort -z) + +if [ "${#windows_artifacts[@]}" -eq 0 ]; then + echo "windows_artifacts is empty; no Windows .exe artifacts found under ${TAURI_DIR}/target/release." >&2 + exit 1 +fi + +if [ "${#windows_runtime_dlls[@]}" -eq 0 ]; then + echo "windows_runtime_dlls is empty; no staged runtime DLLs found under ${TAURI_DIR}/resources/windows." >&2 + exit 1 +fi + +write_sha256_manifest "${repro_dir}/desktop-pr-windows-final.sha256" "${windows_artifacts[@]}" +write_sha256_manifest "${repro_dir}/desktop-pr-windows-runtime-dlls.sha256" "${windows_runtime_dlls[@]}" + +print_file_hashes "${windows_artifacts[@]}" +print_file_hashes "${windows_runtime_dlls[@]}" +verify_frontend_dist_unchanged diff --git a/scripts/ci/verify-release-artifacts.sh b/scripts/ci/verify-release-artifacts.sh index 02e3a390..be3447aa 100755 --- a/scripts/ci/verify-release-artifacts.sh +++ b/scripts/ci/verify-release-artifacts.sh @@ -5,7 +5,7 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" usage() { cat >&2 <<'EOF' -usage: verify-release-artifacts.sh [all|present|linux|macos|android|ios|web|latest-json ...] +usage: verify-release-artifacts.sh [all|present|linux|macos|windows|android|ios|web|latest-json ...] Verifies downloaded release artifacts against their reproducibility proof files. The verifier recomputes final file hashes, canonical signed payload hashes, and @@ -305,6 +305,16 @@ verify_linux_present() { verify_linux_manifest "${final_manifest}" } +verify_windows() { + local final_manifest runtime_manifest + + final_manifest="$(proof_file_required desktop-pr-windows-final.sha256)" + runtime_manifest="$(proof_file_required desktop-pr-windows-runtime-dlls.sha256)" + + verify_file_manifest "${final_manifest}" + verify_file_manifest "${runtime_manifest}" +} + verify_canonical_apple_manifest() { local manifest="$1" local expected_digest="${2:-}" @@ -589,6 +599,7 @@ verify_present() { verify_linux_present fi target_present desktop-release-macos-final.sha256 && verify_macos + target_present desktop-pr-windows-final.sha256 && verify_windows target_present android-release-final.sha256 && verify_android target_present ios-release-final.sha256 && verify_ios target_present web-final.sha256 && verify_web @@ -619,6 +630,9 @@ for target in "$@"; do macos) verify_macos ;; + windows) + verify_windows + ;; android) verify_android ;;