diff --git a/.gitea/workflows/windows-msix.yml b/.gitea/workflows/windows-msix.yml index 1c65866..f99f889 100644 --- a/.gitea/workflows/windows-msix.yml +++ b/.gitea/workflows/windows-msix.yml @@ -1,18 +1,22 @@ -# Build the punktfunk Windows client as a signed MSIX and publish it to Gitea's generic package -# registry, so Windows boxes can download + install a real package (Start tile, clean -# install/uninstall) instead of a loose exe. Runs on the self-hosted Windows runner (host mode; -# scripts/ci/setup-windows-runner.ps1) — the MSVC/WinUI/FFmpeg toolchain + the Windows SDK's -# makeappx/signtool are baked into the runner's daemon env, same as windows.yml. +# Build the punktfunk Windows client as signed MSIX packages (x64 + ARM64) and publish them to +# Gitea's generic package registry, so Windows boxes can download + install a real package (Start +# tile, clean install/uninstall) instead of a loose exe. Runs on the self-hosted Windows runner +# (host mode; scripts/ci/setup-windows-runner.ps1) — the MSVC/WinUI/FFmpeg toolchain + the Windows +# SDK's makeappx/signtool are baked into the runner's daemon env, same as windows.yml. +# +# Both arches come off the ONE x64 runner: x86_64 natively, aarch64 cross-compiled (the x64 MSVC +# toolset has the ARM64 cross compiler; the matrix points FFMPEG_DIR at the ARM64 FFmpeg tree). See +# windows.yml for the cross-build rationale + the BOM/MAX_PATH runner gotchas. # # Registry (public, unom org): https://git.unom.io/unom/-/packages (generic group) -# Packaging internals: clients/windows/packaging/README.md. BOM/MAX_PATH runner -# gotchas baked into the daemon env + windows.yml: see that workflow. +# Packaging internals: clients/windows/packaging/README.md. # # Versioning — MSIX requires a strictly 4-part numeric version (no ~/- suffixes), so: # win-vX.Y.Z tag -> X.Y.Z.0 (a real Windows-client release; `win-v*` is its own tag namespace, # kept off the host's `host-v*` and the Apple `v*` to avoid the # version-shadow class of bug — see deb.yml). # main push / dispatch -> 0.2..0 (rolling; climbs monotonically by run number). +# Both arches share the version; artifacts are arch-suffixed (..._x64.msix / ..._arm64.msix). # # Signing (packaging/pack-msix.ps1): if the MSIX_CERT_PFX_B64 / MSIX_CERT_PASSWORD Actions secrets # are set (a real or shared code-signing .pfx whose subject DN == Publisher), the package is signed @@ -41,17 +45,33 @@ env: jobs: package: runs-on: windows-amd64 - timeout-minutes: 60 + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + target: x86_64-pc-windows-msvc + ffmpeg: C:\Users\Public\ffmpeg + td: C:\t + - arch: arm64 + target: aarch64-pc-windows-msvc + ffmpeg: C:\Users\Public\ffmpeg-arm64 + td: C:\t-a64 steps: - uses: actions/checkout@v4 - name: Configure + version shell: pwsh run: | - # windows-reactor's build.rs unwraps CARGO_WORKSPACE_DIR; CARGO_TARGET_DIR=C:\t dodges the - # MAX_PATH wall in the CMake-from-source crates (see windows.yml). Both via GITHUB_ENV. + # windows-reactor's build.rs unwraps CARGO_WORKSPACE_DIR; CARGO_TARGET_DIR (per-arch, short) + # dodges the MAX_PATH wall in the CMake-from-source crates (see windows.yml). FFMPEG_DIR + # selects the arch's import libs + is read by pack-msix.ps1 for the runtime DLLs. All via + # GITHUB_ENV. "CARGO_WORKSPACE_DIR=$env:GITHUB_WORKSPACE" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - "CARGO_TARGET_DIR=C:\t" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "CARGO_TARGET_DIR=${{ matrix.td }}" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "FFMPEG_DIR=${{ matrix.ffmpeg }}" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + rustup target add ${{ matrix.target }} $parts = if ($env:GITHUB_REF -like 'refs/tags/win-v*') { ($env:GITHUB_REF_NAME -replace '^win-v', '').Split('.') } else { @@ -60,11 +80,11 @@ jobs: while ($parts.Count -lt 4) { $parts += '0' } $v = ($parts[0..3] -join '.') "MSIX_VERSION=$v" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - Write-Output "MSIX version $v" + Write-Output "MSIX version $v arch ${{ matrix.arch }} target ${{ matrix.target }}" - name: Build (release) shell: pwsh - run: cargo build --release -p punktfunk-client-windows + run: cargo build --release -p punktfunk-client-windows --target ${{ matrix.target }} - name: Pack + sign MSIX shell: pwsh @@ -73,7 +93,8 @@ jobs: MSIX_CERT_PASSWORD: ${{ secrets.MSIX_CERT_PASSWORD }} run: | & clients/windows/packaging/pack-msix.ps1 ` - -Version $env:MSIX_VERSION -TargetDir C:\t\release -OutDir C:\t\msix + -Version $env:MSIX_VERSION -Arch ${{ matrix.arch }} ` + -TargetDir ${{ matrix.td }}\${{ matrix.target }}\release -OutDir ${{ matrix.td }}\msix - name: Publish to Gitea generic registry shell: pwsh diff --git a/.gitea/workflows/windows.yml b/.gitea/workflows/windows.yml index 5f848ec..b7f3048 100644 --- a/.gitea/workflows/windows.yml +++ b/.gitea/workflows/windows.yml @@ -1,18 +1,30 @@ # Windows client CI — runs on the self-hosted Windows runner (home-windows-1, host mode; see # scripts/ci/setup-windows-runner.ps1). Build + clippy + fmt + test the WinUI 3 client -# (windows-reactor + D3D11/SwapChainPanel + WASAPI + SDL3) on x86_64-pc-windows-msvc. +# (windows-reactor + D3D11/SwapChainPanel + WASAPI + SDL3). # -# The MSVC/WinUI/FFmpeg toolchain (cargo/rustup on ASCII paths, NASM, CMake, LLVM, FFmpeg, -# CARGO_HOME, CMAKE_POLICY_VERSION_MINIMUM, …) is baked into the runner's daemon env. Two -# per-checkout vars are set in a step: +# Two architectures from ONE x64 runner: x86_64-pc-windows-msvc natively and +# aarch64-pc-windows-msvc by cross-compiling. The x64 MSVC toolset ships an ARM64 cross compiler +# (VC\Tools\MSVC\\bin\Hostx64\arm64\cl.exe) and aarch64-pc-windows-msvc is a tier-2 Rust +# target with host tools, so no ARM64 runner is needed — the cc/cmake crates pick the ARM64 +# compiler from the target triple (SDL3 + libopus build-from-source cross-compile fine). The one +# arch-specific external dep is FFmpeg's import libs: the runner keeps an x64 tree at +# C:\Users\Public\ffmpeg and an ARM64 tree at C:\Users\Public\ffmpeg-arm64 (both FFmpeg 7.x / +# avcodec-61); the matrix points FFMPEG_DIR at the right one. aarch64 can't *run* on the x64 host, +# so fmt + test run only for x64. +# +# The MSVC/WinUI/FFmpeg toolchain (cargo/rustup on ASCII paths, NASM, CMake, LLVM, the x64 FFmpeg, +# CARGO_HOME, CMAKE_POLICY_VERSION_MINIMUM, …) is baked into the runner's daemon env. Per-checkout +# / per-arch vars are set in a step: # - CARGO_WORKSPACE_DIR windows-reactor's build.rs unwraps it + stages the Win App SDK # NuGets/winmd under it (from GITHUB_WORKSPACE). -# - CARGO_TARGET_DIR=C:\t the runner's host workdir is buried deep under +# - CARGO_TARGET_DIR=C:\t… the runner's host workdir is buried deep under # C:\Windows\System32\config\systemprofile\.cache\act\\hostexecutor\, # so the default target\ path blows past Windows' MAX_PATH (260) inside the # CMake-from-source builds (audiopus_sys / SDL3) — MSBuild's tracker then # can't create its .tlog (DirectoryNotFoundException -> MSB6003). A short -# root keeps every nested path well under the limit. +# root keeps every nested path well under the limit (per-arch so the two +# matrix legs don't share a target dir). +# - FFMPEG_DIR per-arch FFmpeg import libs (x64 vs arm64 tree). # # Steps use `shell: pwsh` (PowerShell 7) deliberately: Windows PowerShell 5.1's # `Out-File -Encoding utf8` prepends a UTF-8 BOM that corrupts the first GITHUB_ENV line (the @@ -41,7 +53,11 @@ on: jobs: build: runs-on: windows-amd64 - timeout-minutes: 60 + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + target: [x86_64-pc-windows-msvc, aarch64-pc-windows-msvc] steps: - uses: actions/checkout@v4 @@ -49,24 +65,31 @@ jobs: shell: pwsh run: | "CARGO_WORKSPACE_DIR=$env:GITHUB_WORKSPACE" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - "CARGO_TARGET_DIR=C:\t" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + # Per-arch short target root (dodges MAX_PATH; keeps the two legs from sharing target\). + $td = if ('${{ matrix.target }}' -eq 'aarch64-pc-windows-msvc') { 'C:\t-a64' } else { 'C:\t' } + "CARGO_TARGET_DIR=$td" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + # Per-arch FFmpeg import libs (the runner provisions both — setup-windows-runner.ps1). + $ff = if ('${{ matrix.target }}' -eq 'aarch64-pc-windows-msvc') { 'C:\Users\Public\ffmpeg-arm64' } else { 'C:\Users\Public\ffmpeg' } + "FFMPEG_DIR=$ff" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + rustup target add ${{ matrix.target }} rustc --version cargo --version - node --version - Write-Output "workspace: $env:GITHUB_WORKSPACE" + Write-Output "target ${{ matrix.target }} target-dir $td ffmpeg $ff" - name: Build shell: pwsh - run: cargo build -p punktfunk-client-windows + run: cargo build -p punktfunk-client-windows --target ${{ matrix.target }} - name: Clippy (-D warnings) shell: pwsh - run: cargo clippy -p punktfunk-client-windows --all-targets -- -D warnings + run: cargo clippy -p punktfunk-client-windows --all-targets --target ${{ matrix.target }} -- -D warnings - name: Rustfmt check + if: matrix.target == 'x86_64-pc-windows-msvc' shell: pwsh run: cargo fmt -p punktfunk-client-windows -- --check - name: Test + if: matrix.target == 'x86_64-pc-windows-msvc' shell: pwsh - run: cargo test -p punktfunk-client-windows + run: cargo test -p punktfunk-client-windows --target ${{ matrix.target }} diff --git a/CLAUDE.md b/CLAUDE.md index 7023398..280a118 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,7 +140,11 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc dev VM is headless/WARP; needs the RTX box.)** **Stream input** is Win32 low-level hooks (`WH_KEYBOARD_LL`/`WH_MOUSE_LL`) — reactor exposes no raw key/pointer events; native Windows VK + absolute mouse (client-rect Contain-fit) + wheel, Ctrl+Alt+Shift+Q capture toggle. `--headless`/`--discover` keep CLI paths. Builds + clippy - + fmt green on `x86_64-pc-windows-msvc` (on the dev VM). **windows-reactor is unpublished** (git + + fmt green on **`x86_64-pc-windows-msvc` and `aarch64-pc-windows-msvc`** — the latter + **cross-compiled off the one x64 runner** (no ARM64 runner; the x64 MSVC toolset's ARM64 cross + compiler + a per-arch `FFMPEG_DIR` ARM64 tree, SDL3/libopus build-from-source cross-compile + cleanly), and both ship as signed MSIX (`windows-msix.yml` matrix → `..._x64.msix`/`..._arm64.msix`, + verified: ARM64 binaries + manifest arch). **windows-reactor is unpublished** (git dep pinned to commit `b4129fcc`; `windows` pinned to the SAME commit so `IDXGISwapChain1` unifies with `set_swap_chain`); its `build.rs` downloads the Win App SDK NuGets + needs `CARGO_WORKSPACE_DIR` set (in the VM build env; `/temp`+`/winmd` gitignored). Gotcha: `CARGO_HOME` must be an ASCII path diff --git a/clients/windows/packaging/AppxManifest.xml b/clients/windows/packaging/AppxManifest.xml index 222fa49..6242944 100644 --- a/clients/windows/packaging/AppxManifest.xml +++ b/clients/windows/packaging/AppxManifest.xml @@ -26,7 +26,7 @@ Name="unom.Punktfunk" Publisher="{PUBLISHER}" Version="{VERSION}" - ProcessorArchitecture="x64" /> + ProcessorArchitecture="{ARCH}" /> Punktfunk diff --git a/clients/windows/packaging/README.md b/clients/windows/packaging/README.md index a92e1d8..6e4cdad 100644 --- a/clients/windows/packaging/README.md +++ b/clients/windows/packaging/README.md @@ -1,11 +1,19 @@ # punktfunk Windows client — MSIX packaging -The Windows client ships as a **signed MSIX** so Windows boxes get a real package (Start tile, -clean install/uninstall) instead of a loose exe. CI builds + publishes it from +The Windows client ships as **signed MSIX** packages so Windows boxes get a real package (Start +tile, clean install/uninstall) instead of a loose exe. CI builds + publishes them from [`.gitea/workflows/windows-msix.yml`](../../../.gitea/workflows/windows-msix.yml) to Gitea's **generic** package registry (`https://git.unom.io/unom/-/packages`), on every `main` push that touches the client and on `win-v*` release tags. +**Two architectures, one x64 runner.** Both `x64` and `arm64` packages are produced off the single +x64 Windows runner — `x86_64-pc-windows-msvc` builds natively, `aarch64-pc-windows-msvc` is +cross-compiled (the x64 MSVC toolset ships the ARM64 cross compiler; the matrix points `FFMPEG_DIR` +at the runner's ARM64 FFmpeg tree, `C:\Users\Public\ffmpeg-arm64`). Artifacts are arch-suffixed +(`..._x64.msix` / `..._arm64.msix`, each with its matching `.cer`); `pack-msix.ps1 -Arch x64|arm64` +stamps the manifest `ProcessorArchitecture` and names the output. See +[`windows.yml`](../../../.gitea/workflows/windows.yml) for the cross-build rationale. + ## What's in the package `pack-msix.ps1` assembles a layout from a `cargo build --release` and runs `makeappx` + `signtool`: @@ -47,8 +55,9 @@ trusted with no further prompt: ```powershell # once per machine (elevated): trust the publisher Import-Certificate -FilePath .\punktfunk-codesign.cer -CertStoreLocation Cert:\LocalMachine\TrustedPeople -# then install (and re-run for each upgrade — no re-trust needed) -Add-AppxPackage -Path .\punktfunk-client-windows__x64.msix +# then install the package for your CPU (and re-run for each upgrade — no re-trust needed) +Add-AppxPackage -Path .\punktfunk-client-windows__x64.msix # Intel/AMD +Add-AppxPackage -Path .\punktfunk-client-windows__arm64.msix # ARM64 (Snapdragon, etc.) ``` The matching `.cer` is also published next to each `.msix` in the registry, so it's always at hand. @@ -70,9 +79,16 @@ it changes the package identity → a one-time reinstall). On the Windows runner / dev VM (MSVC + Windows SDK present), after a release build: ```powershell -cargo build --release -p punktfunk-client-windows +# x64 +cargo build --release -p punktfunk-client-windows --target x86_64-pc-windows-msvc pwsh -File clients/windows/packaging/pack-msix.ps1 ` - -Version 0.2.0.0 -TargetDir C:\t\release -OutDir C:\t\msix + -Version 0.2.0.0 -TargetDir C:\t\x86_64-pc-windows-msvc\release -OutDir C:\t\msix + +# arm64 (cross-compiled; point FFMPEG_DIR at the ARM64 tree) +$env:FFMPEG_DIR = 'C:\Users\Public\ffmpeg-arm64' +cargo build --release -p punktfunk-client-windows --target aarch64-pc-windows-msvc +pwsh -File clients/windows/packaging/pack-msix.ps1 ` + -Version 0.2.0.0 -Arch arm64 -TargetDir C:\t\aarch64-pc-windows-msvc\release -OutDir C:\t\msix ``` Validated end-to-end on the build VM (pack → sign → `Add-AppxPackage` → framework-dependency diff --git a/clients/windows/packaging/pack-msix.ps1 b/clients/windows/packaging/pack-msix.ps1 index b0cb16b..4c72ce5 100644 --- a/clients/windows/packaging/pack-msix.ps1 +++ b/clients/windows/packaging/pack-msix.ps1 @@ -18,12 +18,17 @@ Run on the Windows runner (or the dev VM) with the MSVC/Windows SDK present. .EXAMPLE - pwsh -File pack-msix.ps1 -Version 0.2.137.0 -TargetDir C:\t\release -FfmpegBin C:\Users\Public\ffmpeg\bin -OutDir C:\t\msix + # x64 (default arch): + pwsh -File pack-msix.ps1 -Version 0.2.137.0 -TargetDir C:\t\x86_64-pc-windows-msvc\release -OutDir C:\t\msix + # arm64 (point -TargetDir + FFMPEG_DIR at the ARM64 build/tree): + $env:FFMPEG_DIR='C:\Users\Public\ffmpeg-arm64' + pwsh -File pack-msix.ps1 -Version 0.2.137.0 -Arch arm64 -TargetDir C:\t-a64\aarch64-pc-windows-msvc\release -OutDir C:\t-a64\msix #> [CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$Version, # 4-part numeric, e.g. 0.2.137.0 [Parameter(Mandatory = $true)][string]$TargetDir, # cargo --release output dir (has the exe) + [ValidateSet('x64', 'arm64')][string]$Arch = 'x64', # package ProcessorArchitecture + artifact suffix [string]$FfmpegBin = $(if ($env:FFMPEG_DIR) { Join-Path $env:FFMPEG_DIR 'bin' } else { 'C:\Users\Public\ffmpeg\bin' }), [string]$OutDir = (Join-Path $TargetDir 'msix'), [string]$Publisher = 'CN=unom', # MUST equal the signing cert subject DN @@ -79,8 +84,8 @@ $ff | ForEach-Object { Copy-Item $_.FullName (Join-Path $layout $_.Name) -Force # tile/store assets Copy-Item (Join-Path $assets '*') (Join-Path $layout 'Assets') -Force -# manifest with version + publisher substituted -$manifest = (Get-Content -Raw $manifestTemplate).Replace('{VERSION}', $Version).Replace('{PUBLISHER}', $Publisher) +# manifest with version + publisher + architecture substituted +$manifest = (Get-Content -Raw $manifestTemplate).Replace('{VERSION}', $Version).Replace('{PUBLISHER}', $Publisher).Replace('{ARCH}', $Arch) Set-Content -Path (Join-Path $layout 'AppxManifest.xml') -Value $manifest -Encoding UTF8 Write-Host "layout assembled at $layout :" @@ -88,13 +93,13 @@ Get-ChildItem $layout -Recurse -File | ForEach-Object { " $($_.FullName.Substri # --- pack --- New-Item -ItemType Directory -Force -Path $OutDir | Out-Null -$msix = Join-Path $OutDir "punktfunk-client-windows_${Version}_x64.msix" +$msix = Join-Path $OutDir "punktfunk-client-windows_${Version}_${Arch}.msix" & $makeappx pack /o /d $layout /p $msix if ($LASTEXITCODE -ne 0) { throw "makeappx pack failed ($LASTEXITCODE)" } # --- signing cert (supplied stable pfx OR ephemeral self-signed) --- $pfxPath = Join-Path $OutDir 'signing.pfx' -$cerPath = Join-Path $OutDir "punktfunk-client-windows_${Version}_x64.cer" +$cerPath = Join-Path $OutDir "punktfunk-client-windows_${Version}_${Arch}.cer" if ($PfxBase64) { Write-Host "signing with supplied code-signing cert (MSIX_CERT_PFX_B64)" [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($PfxBase64)) diff --git a/docs/windows-client-bootstrap.md b/docs/windows-client-bootstrap.md index 403d468..42bc1ce 100644 --- a/docs/windows-client-bootstrap.md +++ b/docs/windows-client-bootstrap.md @@ -8,9 +8,10 @@ the gotchas. Read it top to bottom, then start at **Phase 1** (de-risk Reactor f ## Status — WinUI 3 client landed (2026-06-15) The client is implemented in `clients/windows` (binary `punktfunk-client`) and is -**build + clippy + fmt green on `x86_64-pc-windows-msvc`** (built on the dev VM). It is the **WinUI 3** -client this doc planned: native chrome (host list, settings, in-app SPAKE2 PIN pairing) + the video on -a **`SwapChainPanel`**, all in pure Rust. +**build + clippy + fmt green on `x86_64-pc-windows-msvc` and `aarch64-pc-windows-msvc`** (the ARM64 +target cross-compiled off the one x64 runner — see `windows.yml`; signed MSIX for both arches via +`windows-msix.yml`). It is the **WinUI 3** client this doc planned: native chrome (host list, +settings, in-app SPAKE2 PIN pairing) + the video on a **`SwapChainPanel`**, all in pure Rust. - **Reactor is viable after all — it is what we use.** The locked decision held. windows-rs [PR #4499](https://github.com/microsoft/windows-rs/pull/4499) (merged 2026-06-01) added a diff --git a/scripts/ci/setup-windows-runner.ps1 b/scripts/ci/setup-windows-runner.ps1 index 2898428..fc077c5 100644 --- a/scripts/ci/setup-windows-runner.ps1 +++ b/scripts/ci/setup-windows-runner.ps1 @@ -84,6 +84,41 @@ if (-not (Test-Path "C:\Users\Public\.rustup\settings.toml")) { robocopy "$env:USERPROFILE\.rustup" "C:\Users\Public\.rustup" /E /NFL /NDL /NJH /NJS /MT:16 | Out-Null } +# --- ARM64 cross-compile support (windows.yml / windows-msix.yml build aarch64-pc-windows-msvc off +# this x64 box; no ARM64 runner needed). Two pieces beyond the x64 toolchain: +# 1. the rustup std for the target; +# 2. an ARM64 FFmpeg import-lib/DLL tree at C:\Users\Public\ffmpeg-arm64 (the workflow matrix +# points FFMPEG_DIR here for the aarch64 leg; the x64 tree stays at C:\Users\Public\ffmpeg). +# The x64 MSVC toolset already ships the ARM64 cross compiler — if +# VC\Tools\MSVC\\bin\Hostx64\arm64\cl.exe is missing, add the VS "MSVC v143+ ARM64/ARM64EC +# build tools" + "C++ ARM64 build tools" workload components (the cc/cmake crates need it to +# cross-build SDL3 + libopus). +$env:RUSTUP_HOME = "C:\Users\Public\.rustup" +$env:CARGO_HOME = "C:\Users\Public\.cargo" +$rustup = (Get-Command rustup -ErrorAction SilentlyContinue).Source +if (-not $rustup) { $rustup = "C:\Users\Public\.cargo\bin\rustup.exe" } +if (Test-Path $rustup) { + Write-Host "==> rustup target add aarch64-pc-windows-msvc" + & $rustup target add aarch64-pc-windows-msvc +} else { Write-Warning "rustup not found - install rustup then re-run (needed for the aarch64 target)." } + +$ffArm = "C:\Users\Public\ffmpeg-arm64" +if (-not (Test-Path (Join-Path $ffArm 'lib\avcodec.lib'))) { + # BtbN winarm64 shared, FFmpeg 7.x (avcodec-61) to match the x64 tree's ABI. MSVC-linkable .lib + # import libs + headers + bin\*.dll — exactly what ffmpeg-sys-next + pack-msix.ps1 consume. + Write-Host "==> fetching ARM64 FFmpeg (BtbN winarm64 shared)" + $ffUrl = 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.1-latest-winarm64-gpl-shared-7.1.zip' + $ffZip = "C:\Users\Public\ffmpeg-arm64.zip" + $ffTmp = "C:\Users\Public\ffmpeg-arm64-extract" + Invoke-WebRequest -Uri $ffUrl -OutFile $ffZip -UseBasicParsing + if (Test-Path $ffTmp) { Remove-Item -Recurse -Force $ffTmp } + Expand-Archive -Path $ffZip -DestinationPath $ffTmp -Force # BtbN zips have one top-level folder + $inner = Get-ChildItem $ffTmp -Directory | Select-Object -First 1 + if (Test-Path $ffArm) { Remove-Item -Recurse -Force $ffArm } + Move-Item -Path $inner.FullName -Destination $ffArm + Remove-Item -Force $ffZip; Remove-Item -Recurse -Force $ffTmp -ErrorAction SilentlyContinue +} + # Inno Setup (ISCC.exe) for the host installer build (windows-host.yml). pack-host-installer.ps1 # locates it at its fixed Program Files path, so it need not be on PATH — just present. if (-not (Test-Path "C:\Program Files (x86)\Inno Setup 6\ISCC.exe")) {