From bb11b2faf75167f4a65c477cb1c83b1612f0ea98 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Tue, 16 Jun 2026 08:45:43 +0000 Subject: [PATCH] feat(windows): MSIX packaging + publish workflow for the WinUI client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Package the Windows client as a signed MSIX (Start tile, clean install/uninstall) and publish it to Gitea's generic registry, mirroring the host's .deb/.rpm and the Mac's DMG. Validated end-to-end on the build VM: cargo build --release -> makeappx pack (16 payload files, 58 MB) -> signtool -> Add-AppxPackage deploy -> framework-dependency resolution all green. - packaging/AppxManifest.xml: full-trust Win32 app (Windows.FullTrustApplication + runFullTrust), templated {VERSION}/{PUBLISHER}. windows-reactor packages cleanly despite being built "unpackaged" because it calls MddBootstrapInitialize2 with OnPackageIdentity_NOOP — under MSIX identity the bootstrapper no-ops and the App SDK resolves from the manifest's PackageDependency on Microsoft.WindowsAppRuntime.2 (reactor pins MAJORMINOR 0x20000 = 2.0). - packaging/pack-msix.ps1: assemble layout (exe + reactor/SDL3 auto-staged DLLs + resources.pri + FFmpeg DLLs + tile assets), makeappx, signtool. Cert precedence: MSIX_CERT_PFX_B64 secret, else an ephemeral self-signed cert whose .cer is published alongside (swap in a real cert later, no manifest change). - assets: tile/store logos rasterized from packaging/flatpak/io.unom.Punktfunk.svg. - .gitea/workflows/windows-msix.yml: runs on the Windows runner on main pushes + win-v* tags + dispatch. MSIX version is 4-part numeric — win-vX.Y.Z -> X.Y.Z.0, else 0.2..0. shell: pwsh + CARGO_TARGET_DIR=C:\t like windows.yml. Co-Authored-By: Claude Opus 4.8 --- .gitea/workflows/windows-msix.yml | 90 ++++++++++++ .../packaging/AppxManifest.xml | 65 ++++++++ .../packaging/README.md | 71 +++++++++ .../packaging/assets/Square150x150Logo.png | Bin 0 -> 3202 bytes .../packaging/assets/Square44x44Logo.png | Bin 0 -> 1046 bytes .../packaging/assets/Square71x71Logo.png | Bin 0 -> 1659 bytes .../packaging/assets/StoreLogo.png | Bin 0 -> 1166 bytes .../packaging/pack-msix.ps1 | 139 ++++++++++++++++++ 8 files changed, 365 insertions(+) create mode 100644 .gitea/workflows/windows-msix.yml create mode 100644 crates/punktfunk-client-windows/packaging/AppxManifest.xml create mode 100644 crates/punktfunk-client-windows/packaging/README.md create mode 100644 crates/punktfunk-client-windows/packaging/assets/Square150x150Logo.png create mode 100644 crates/punktfunk-client-windows/packaging/assets/Square44x44Logo.png create mode 100644 crates/punktfunk-client-windows/packaging/assets/Square71x71Logo.png create mode 100644 crates/punktfunk-client-windows/packaging/assets/StoreLogo.png create mode 100644 crates/punktfunk-client-windows/packaging/pack-msix.ps1 diff --git a/.gitea/workflows/windows-msix.yml b/.gitea/workflows/windows-msix.yml new file mode 100644 index 0000000..c8875ea --- /dev/null +++ b/.gitea/workflows/windows-msix.yml @@ -0,0 +1,90 @@ +# 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. +# +# Registry (public, unom org): https://git.unom.io/unom/-/packages (generic group) +# Packaging internals: crates/punktfunk-client-windows/packaging/README.md. BOM/MAX_PATH runner +# gotchas baked into the daemon env + windows.yml: see that workflow. +# +# 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). +# +# 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 +# with them. Otherwise an ephemeral self-signed cert is generated and its public .cer is published +# next to the .msix (users import it to Trusted People before install). Drop in a real cert later +# with no workflow change — just add the secrets (+ pass -Publisher if its subject differs). +name: windows-msix + +on: + push: + branches: [main] + paths: + - 'crates/punktfunk-client-windows/**' + - 'crates/punktfunk-core/**' + - 'Cargo.lock' + - 'Cargo.toml' + - '.gitea/workflows/windows-msix.yml' + tags: ['win-v*'] + workflow_dispatch: + +env: + REGISTRY: git.unom.io + OWNER: unom + PKG: punktfunk-client-windows + +jobs: + package: + runs-on: windows-amd64 + timeout-minutes: 60 + 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. + "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 + $parts = if ($env:GITHUB_REF -like 'refs/tags/win-v*') { + ($env:GITHUB_REF_NAME -replace '^win-v', '').Split('.') + } else { + @('0', '2', $env:GITHUB_RUN_NUMBER) + } + 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" + + - name: Build (release) + shell: pwsh + run: cargo build --release -p punktfunk-client-windows + + - name: Pack + sign MSIX + shell: pwsh + env: + MSIX_CERT_PFX_B64: ${{ secrets.MSIX_CERT_PFX_B64 }} + MSIX_CERT_PASSWORD: ${{ secrets.MSIX_CERT_PASSWORD }} + run: | + & crates/punktfunk-client-windows/packaging/pack-msix.ps1 ` + -Version $env:MSIX_VERSION -TargetDir C:\t\release -OutDir C:\t\msix + + - name: Publish to Gitea generic registry + shell: pwsh + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + $files = @($env:MSIX_PATH, $env:MSIX_CER_PATH) | Where-Object { $_ -and (Test-Path $_) } + if (-not $files) { throw "pack produced no artifacts to publish" } + foreach ($f in $files) { + $name = Split-Path $f -Leaf + $url = "https://$($env:REGISTRY)/api/packages/$($env:OWNER)/generic/$($env:PKG)/$($env:MSIX_VERSION)/$name" + curl.exe -fsS --user "enricobuehler:$($env:REGISTRY_TOKEN)" --upload-file "$f" "$url" + Write-Output "published $name -> $url" + } diff --git a/crates/punktfunk-client-windows/packaging/AppxManifest.xml b/crates/punktfunk-client-windows/packaging/AppxManifest.xml new file mode 100644 index 0000000..222fa49 --- /dev/null +++ b/crates/punktfunk-client-windows/packaging/AppxManifest.xml @@ -0,0 +1,65 @@ + + + + + + + + Punktfunk + unom + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/punktfunk-client-windows/packaging/README.md b/crates/punktfunk-client-windows/packaging/README.md new file mode 100644 index 0000000..f42b598 --- /dev/null +++ b/crates/punktfunk-client-windows/packaging/README.md @@ -0,0 +1,71 @@ +# 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 +[`.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. + +## What's in the package + +`pack-msix.ps1` assembles a layout from a `cargo build --release` and runs `makeappx` + `signtool`: + +| File | Source | +|---|---| +| `punktfunk-client.exe` | the release build | +| `Microsoft.WindowsAppRuntime.Bootstrap.dll`, `resources.pri` | auto-staged by windows-reactor's `build.rs` | +| `SDL3.dll` | auto-staged by the `sdl3` crate | +| `avcodec/avformat/avutil/swscale/swresample/...-*.dll` | `FFMPEG_DIR\bin` | +| `Assets\*.png` | checked-in tile/store logos (rasterized from `packaging/flatpak/io.unom.Punktfunk.svg`) | +| `AppxManifest.xml` | the template here, with `{VERSION}`/`{PUBLISHER}` substituted | + +### Why an "unpackaged" WinUI app packages cleanly + +windows-reactor calls `MddBootstrapInitialize2` with `OnPackageIdentity_NOOP` +(`crates/libs/reactor/src/app.rs`), so under MSIX **package identity** the App SDK bootstrapper is +a no-op and the runtime is resolved from the manifest's `` on +`Microsoft.WindowsAppRuntime.2` instead (reactor pins `WINDOWSAPPSDK_RELEASE_MAJORMINOR = 0x20000` += 2.0). It's a full-trust Win32 app (`EntryPoint="Windows.FullTrustApplication"` + `runFullTrust`) +because it owns raw D3D11, Win32 low-level input hooks, WASAPI and SDL3. + +## Versioning + +MSIX requires a strictly 4-part numeric version. The workflow computes: +- `win-vX.Y.Z` tag → `X.Y.Z.0` (a real client release; `win-v*` is its own tag namespace, kept off + the host's `host-v*` and Apple's `v*` to avoid the version-shadow bug). +- `main` push / `workflow_dispatch` → `0.2..0` (rolling, climbs by run number). + +## Signing & install + +Signing precedence in `pack-msix.ps1`: +1. **`MSIX_CERT_PFX_B64` / `MSIX_CERT_PASSWORD`** Actions secrets — a real (or shared) code-signing + `.pfx` whose **subject DN must equal `-Publisher`** (default `CN=unom`). Drop these in later to + move off self-signed with **no manifest change**; if the cert's subject differs, pass a matching + `-Publisher` (it's stamped into the manifest `Identity`). +2. otherwise an **ephemeral self-signed** cert (subject = `-Publisher`) is generated and its public + `.cer` is published next to the `.msix`. + +To install a self-signed build, trust the cert once, then add the package: + +```powershell +Import-Certificate -FilePath .\punktfunk-client-windows__x64.cer -CertStoreLocation Cert:\LocalMachine\TrustedPeople +Add-AppxPackage -Path .\punktfunk-client-windows__x64.msix +``` + +The MSIX declares a dependency on the Windows App SDK 2.x runtime; install +[the App SDK runtime](https://aka.ms/windowsappsdk) if `Add-AppxPackage` reports a missing +`Microsoft.WindowsAppRuntime.2` framework. + +## Building locally + +On the Windows runner / dev VM (MSVC + Windows SDK present), after a release build: + +```powershell +cargo build --release -p punktfunk-client-windows +pwsh -File crates/punktfunk-client-windows/packaging/pack-msix.ps1 ` + -Version 0.2.0.0 -TargetDir C:\t\release -OutDir C:\t\msix +``` + +Validated end-to-end on the build VM (pack → sign → `Add-AppxPackage` → framework-dependency +resolution). The only step that needs a real display is *launching* the WinUI window (same +on-glass constraint as the rest of the client). diff --git a/crates/punktfunk-client-windows/packaging/assets/Square150x150Logo.png b/crates/punktfunk-client-windows/packaging/assets/Square150x150Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6113c5e75b8f36822a0d27cfa88625536f40a415 GIT binary patch literal 3202 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 literal 0 HcmV?d00001 diff --git a/crates/punktfunk-client-windows/packaging/assets/Square44x44Logo.png b/crates/punktfunk-client-windows/packaging/assets/Square44x44Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3b3f2d8e922f7ffc20eb3d872df064444b9b12b5 GIT binary patch literal 1046 zcmV+x1nK*UP)>CSqcisk@O~iN~lHc({}V<00{MBh|#j zD-T!V8H_PT6Dl-TqrztZkEuaaY6L+=3(}VQe_IQPAMM-nHMEJp@TLFfYeIi{cs|}2 zUIinVmT4(c22jda72(PorJBe zt?Db5qDDa0`wq2?7G8;nYM6vMLOLM)eTL;sqIFgS>(dT(gCb@YzCd>qpH$3I*xj@2 z?n0KyfK{0`#o}j37Sv=D#uwq?m@*1@6=Gt#iB*u!u12^z?0Eg=A*<+#i?EQK`BT{^ zq9bv=s7FJ~4tP9oS;?cj@XVjfkej`jDKZ{!59~MBN7Zdzg|ny5M}A&8lRQGY*0BQa zUKgv#=q4mwf?KN>*K8@X@qY&(Ibd;6Heql@qlxqo7x)ynia_wV@NsP~}%J&Q1OcB>qVGN$m1 zSvV_e2L71+FIKJopOrQ*I|g98?BVO^9~D<&qtS%oxhs*E_ZQOBzlMLn53||8cSc7@ zpF7<@YNz8STv$+o1q(JZ4dM0rG3(dx!h$B%3rC%o>2uoJFQB1$2UB>W5?)Br0t##K ztx>^unJG-+Ief5Bv&&-DYkPKqDLm4`X7k@DU9y`=n-p%Uf60s7Wk;Qdo%S&e;qH6R z#bcS&GA%8_FCTgV_K9I3k@{Ot0KA5$@5Lf}tD zO=}XkYAw;*$l+Mi^`gPYu(Uz7vIGqjX?d{-MBOfTX)MdQ|Sc9frOGw{CxBad!B0QVx2`Ak}g QQUCw|07*qoM6N<$f?z20288*EP)^5|mscblN~uC52|*S`Aq!1WgdiKfzfBOyCZeOZJF<+!lY+nJn}umo zxgzKT{9_h+OmWIepv@h(^y- zv_g!9;tx4-2Ca4%Bru$?n34af9#O)GS3Crb%UO^v)uLib5#t(xC3dq*-henJ2VzPg zrWVnFG0|#n(k?lFLym2;6@@*p2$p-4j}KuF!1Cc%V|ZB zFbq67%W@P1124Gm7;^kz$nnFJkdr*!j4_vbdjF!~;XWLR3^6IkoL)e4QuD~1RtPC( z9Py;5_d4C}Zl!knFVu7YI*x>gn3BUnmM_^%b5im!3l5fa)p~^P-s6+5zQlx_q@xd>ae!g(B-S2A?Zid<*=B76^H1lS=pFrgRAJGyj!=v6jeETigCe$&RyDp zq@AeB$Yn1tA|hV+@J%&w=X)o znvm3?i=6WEeOM|*NWsBNYiYKLrcD!=g$`E@SC2pX2X9kd)30zqn)7jG+gc*Box&NXk>Pv0P%VeAZ9e+&43fUEq$Ctj`dt5& zwD043>aaKCLhqEW)#8q7Ia-@>Ft8}XAEvLjo$4EEk%%t*b|P#-+)*uu(Q{b^qUy_S zWVN;+MGixbA2i9SsQ705CRVt4@BLyL80bZc9Gc{;Dc<6|Zxe2=Txq20qi-Wc7EN+^ z+rZn0aP!m82dJs(JEX{>Ne=Vo*2A=TaXwOb7#!@Qy`OA|_>y8Y%L#Y&)YR;uOU);d zB9CS{j2?R|U);FSLSIyELy8W%$YC~>?{_|F&>~jkm4sERMt$VU^gbau^om=%?yw6{Dvi>>OgOMdkF3AwAeIbbVvXI002ovPDHLk FV1gdDFdYB@ literal 0 HcmV?d00001 diff --git a/crates/punktfunk-client-windows/packaging/assets/StoreLogo.png b/crates/punktfunk-client-windows/packaging/assets/StoreLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..2c06f608dc278354671cb00975e843cf11f5f4df GIT binary patch literal 1166 zcmV;91abR`P)3{BV5G$=lwDcUA}OJ6OhZap}l%-3ejXjL9#-^m$G!%?WsT3lGU3Ppw*4ZMk>_5A+bf@W)O!oce zX6MO&oP&mC*ifqbwJ2H`{icZuWCw<(!7y{sHT#{G-w+<`YA@8hil<%)+Hh)v)eIl6cOAMNrjSP9VJEgF~tZ+rZ9Hdz{Hh7 zR_T#(k^KBZRF*fP@~$T2=iSCLOZjJd6lc!u$AyvaSfxfrMQ&eLg$K92fi(p_R>>K= zdSRQu zW@`t^%N}D&os+-4i3=Ci$Bg45G9IKg(nThK-e5D68jg!R+V~P0>Yv8?k{njR=*1`| zub4PLG>6$4p0GjRPtPe&&5nz__ihjF-r{9i%86qU{Axs$g=9)TbM6zSsbL0)aoepos`qjE#?#xD<-w5M@^CW*HYLE8C2==RZV2!P0f&7CsfI+RPhJ1!TPZc$AMj z5+JMm=wCsm*h+P#Fp zkuc8w&PN}Mu+yysTO!*Vcc76+GR!#>Tkp+dN{&p)ax6qzqc_-OYrkbn?MMOdd_}I8S53z@`h-$Z5 zj2SiI_2eWRGt#Z(keezQ7qZ7ng^Y_>$hcX%M#I`d4biCVyQX;-`0DF#adhCblm#qS zO=KZMLnrakz89ICLs_J*?ioD!L_1UZTvriU2zRxA%9K9e9O^_c_&JkvD2oUkyT3$H zk&h`o-g^5fjE|=`CKh8$pAtezD88foBB$@z3 zKxU`lKsTnRCwV4wq$Oe@Q?u;F3T`cA6=P&{5T{S~Gp!Deh-3pdkcd||z`Ijs_oCq! zjjyH;@V23arE7`33@z3Dm_r|H_ad*S`2&uV0DyWPZlr%WWccSr{`-U@VDkM8OV_}Q zpU4IL@lQAex5@Cr;}R^TzJ|!~!AvJ(J&$arKM!cPB!vHoSjYsRMbT=?rybccHOmx> g`47(yEcfBrzt2|MvT;N%F8}}l07*qoM6N<$f>l-}od5s; literal 0 HcmV?d00001 diff --git a/crates/punktfunk-client-windows/packaging/pack-msix.ps1 b/crates/punktfunk-client-windows/packaging/pack-msix.ps1 new file mode 100644 index 0000000..dce9ad7 --- /dev/null +++ b/crates/punktfunk-client-windows/packaging/pack-msix.ps1 @@ -0,0 +1,139 @@ +<# +.SYNOPSIS + Assemble, pack and sign the punktfunk Windows client as a signed MSIX. + +.DESCRIPTION + Builds a packaging layout from a release `cargo build` output (exe + the reactor/SDL3 auto-staged + DLLs + resources.pri + FFmpeg DLLs + the checked-in Assets + the manifest), runs makeappx, and + signs with signtool. Idempotent; safe to re-run. + + Signing cert precedence: + 1. -PfxBase64 / -PfxPassword (a real or shared code-signing cert, e.g. from CI secrets) — the + cert's subject DN MUST match -Publisher (which is stamped into the manifest Identity). + 2. otherwise an EPHEMERAL self-signed code-signing cert with subject = -Publisher is generated + in-process. The package installs only where that cert is trusted, so the matching public + .cer is exported next to the .msix for the user to import (Trusted People) before install. + Swap in a real cert later with zero manifest changes — just pass -PfxBase64/-Publisher. + + 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 +#> +[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) + [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 + [string]$PfxBase64 = $env:MSIX_CERT_PFX_B64, # optional: base64 of a code-signing .pfx + [string]$PfxPassword = $env:MSIX_CERT_PASSWORD +) +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +if ($Version -notmatch '^\d+\.\d+\.\d+\.\d+$') { + throw "Version must be 4-part numeric (Major.Minor.Build.Revision); got '$Version'." +} + +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$assets = Join-Path $here 'assets' +$manifestTemplate = Join-Path $here 'AppxManifest.xml' + +# --- locate the Windows SDK tools (newest makeappx/signtool under the x64 kit bin) --- +function Find-SdkTool([string]$name) { + $root = 'C:\Program Files (x86)\Windows Kits\10\bin' + # match only versioned x64 kit bins (…\10\bin\10.0.NNNNN.N\x64\tool.exe) and pick the newest + $hit = Get-ChildItem -Path $root -Recurse -Filter $name -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match '\\(10\.0\.\d+\.\d+)\\x64\\' } | + Sort-Object { [version]([regex]::Match($_.FullName, '\\(10\.0\.\d+\.\d+)\\x64\\').Groups[1].Value) } | + Select-Object -Last 1 + if (-not $hit) { throw "$name not found under $root — install the Windows 10/11 SDK." } + $hit.FullName +} +$makeappx = Find-SdkTool 'makeappx.exe' +$signtool = Find-SdkTool 'signtool.exe' +Write-Host "makeappx: $makeappx" +Write-Host "signtool: $signtool" + +# --- assemble the package layout --- +$layout = Join-Path $OutDir 'layout' +if (Test-Path $layout) { Remove-Item -Recurse -Force $layout } +New-Item -ItemType Directory -Force -Path (Join-Path $layout 'Assets') | Out-Null + +# binary + auto-staged runtime bits (reactor stages the App SDK bootstrap DLL + resources.pri, +# the sdl3 crate stages SDL3.dll — see crate build output). +$required = @('punktfunk-client.exe', 'Microsoft.WindowsAppRuntime.Bootstrap.dll', 'SDL3.dll', 'resources.pri') +foreach ($f in $required) { + $src = Join-Path $TargetDir $f + if (-not (Test-Path $src)) { throw "missing build artifact '$f' in $TargetDir (did 'cargo build --release' run?)" } + Copy-Item $src (Join-Path $layout $f) -Force +} + +# FFmpeg runtime DLLs (the exe link-imports the decode set; copy them all — small and correct) +$ff = Get-ChildItem -Path $FfmpegBin -Filter *.dll -ErrorAction SilentlyContinue +if (-not $ff) { throw "no FFmpeg DLLs in $FfmpegBin" } +$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) +Set-Content -Path (Join-Path $layout 'AppxManifest.xml') -Value $manifest -Encoding UTF8 + +Write-Host "layout assembled at $layout :" +Get-ChildItem $layout -Recurse -File | ForEach-Object { " $($_.FullName.Substring($layout.Length + 1))" } + +# --- pack --- +New-Item -ItemType Directory -Force -Path $OutDir | Out-Null +$msix = Join-Path $OutDir "punktfunk-client-windows_${Version}_x64.msix" +& $makeappx pack /o /d $layout /p $msix +if ($LASTEXITCODE -ne 0) { throw "makeappx pack failed ($LASTEXITCODE)" } + +# --- signing cert --- +$pfxPath = Join-Path $OutDir 'signing.pfx' +$cerPath = Join-Path $OutDir "punktfunk-client-windows_${Version}_x64.cer" +$selfSigned = $false +if ($PfxBase64) { + Write-Host "signing with supplied code-signing cert (MSIX_CERT_PFX_B64)" + [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($PfxBase64)) +} else { + Write-Host "no MSIX_CERT_PFX_B64 -> generating an ephemeral self-signed cert (subject $Publisher)" + $selfSigned = $true + if (-not $PfxPassword) { $PfxPassword = 'punktfunk' } + $cert = New-SelfSignedCertificate -Type Custom -Subject $Publisher ` + -KeyUsage DigitalSignature -FriendlyName 'punktfunk MSIX (self-signed)' ` + -CertStoreLocation 'Cert:\CurrentUser\My' ` + -TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3', '2.5.29.19={text}') + $sec = ConvertTo-SecureString -String $PfxPassword -Force -AsPlainText + Export-PfxCertificate -Cert "Cert:\CurrentUser\My\$($cert.Thumbprint)" -FilePath $pfxPath -Password $sec | Out-Null + Export-Certificate -Cert "Cert:\CurrentUser\My\$($cert.Thumbprint)" -FilePath $cerPath | Out-Null + Remove-Item "Cert:\CurrentUser\My\$($cert.Thumbprint)" -Force +} + +# --- sign (timestamp best-effort; a self-signed cert needs no TSA) --- +$signArgs = @('sign', '/fd', 'SHA256', '/f', $pfxPath) +if ($PfxPassword) { $signArgs += @('/p', $PfxPassword) } +& $signtool ($signArgs + @('/tr', 'http://timestamp.digicert.com', '/td', 'SHA256', $msix)) +if ($LASTEXITCODE -ne 0) { + Write-Warning "timestamped sign failed — retrying without a timestamp" + & $signtool ($signArgs + @($msix)) + if ($LASTEXITCODE -ne 0) { throw "signtool sign failed ($LASTEXITCODE)" } +} +Remove-Item $pfxPath -Force -ErrorAction SilentlyContinue + +# if self-signed, the public .cer must travel with the package; otherwise drop it (chain is public) +if (-not $selfSigned) { Remove-Item $cerPath -Force -ErrorAction SilentlyContinue } + +Write-Host "" +Write-Host "==> MSIX: $msix" +if ($selfSigned) { + Write-Host "==> self-signed; trust before install: Import-Certificate -FilePath '$cerPath' -CertStoreLocation Cert:\LocalMachine\TrustedPeople" +} +# emit paths for the workflow to publish (only under CI, where GITHUB_ENV is set) +if ($env:GITHUB_ENV) { + "MSIX_PATH=$msix" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + if ($selfSigned) { "MSIX_CER_PATH=$cerPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 } +}