Azure Artifact Signing chains to a public root, so neither the host installer nor the client MSIX ships a .cer any more and users import nothing. install.md, install-client.md and windows-host.md still walked through importing one — and because the pre-Azure files are still sitting in the package registry those URLs return 200 rather than 404, so following the docs didn't fail loudly, it quietly planted a retired self-signed cert in machine Root and TrustedPublisher. Verified on glass against the 0.29 canary while checking the release: both artifacts verify Valid, timestamped, and publicly trusted, and the MSIX installs with nothing imported. Also corrected while here: the MSIX publisher change makes a different package identity, so installs from 0.28.1 or earlier need an uninstall rather than an upgrade (and a packaged app's settings go with it, so the client pairs again); a silent UPGRADE reuses the task selection the previous install recorded instead of the wizard defaults, so a once-declined installgamepad silently keeps stale gamepad drivers; installaudiocable stopped being a task name in 4a621de6; and Add-AppxPackage from a non-interactive session can fail 0x80070005 when the Windows App Runtime it depends on is in use.
punktfunk Windows client — MSIX packaging
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-client.yml to Gitea's
generic package registry (https://git.unom.io/unom/-/packages), on every main push that
touches the client (canary) and on vX.Y.Z release tags (stable) — see
Release Channels.
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; since M10 nothing in the
package links FFmpeg, so neither arch needs a per-arch FFMPEG_DIR tree staged on the runner —
one less thing the ARM64 leg can be missing). Artifacts are arch-suffixed
(..._x64.msix / ..._arm64.msix, plus a matching .cer only in the fallback signing modes 2 and 3
— Azure signing emits none); pack-msix.ps1 -Arch x64|arm64
stamps the manifest ProcessorArchitecture and names the output. See
windows-client.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:
| File | Source |
|---|---|
punktfunk-client.exe |
the release build (the WinUI shell) |
punktfunk-session.exe |
the release build — the Vulkan session client the shell spawns for every stream (sibling resolution, src/spawn.rs). Skia links statically; vulkan-1.dll is a GPU-driver component, never bundled. ARM64 builds it --no-default-features (no Skia console UI) until rust-skia ships aarch64-pc-windows-msvc prebuilts |
Microsoft.WindowsAppRuntime.Bootstrap.dll, resources.pri |
staged by the client's build.rs via windows-reactor-setup::as_framework_dependent() |
SDL3.dll |
auto-staged by the sdl3 crate |
licenses\* |
the project's MIT/Apache texts + the generated THIRD-PARTY-NOTICES.txt (MSIX has no installer EULA page, so attribution ships as files) |
Assets\*.png |
checked-in tile/store logos (rasterized from packaging/flatpak/io.unom.Punktfunk.svg) |
AppxManifest.xml |
the template here, with {VERSION}/{PUBLISHER} substituted |
No FFmpeg DLLs. The client decodes natively since M10 (pf-vkdecode / pf-dxvadec /
OpenH264+rav1d — punktfunk-planning design/client-native-decode.md §6), so nothing here
link-imports libav* and the wildcard avcodec/avformat/avutil/swscale/swresample-*.dll copy is
gone, along with the FFmpeg LGPL notice that accompanied it — shipping that notice now would claim
a dependency the package doesn't have. The host installer is unchanged:
packaging/windows/pack-host-installer.ps1 still ships those DLLs for its AMF/QSV encode path.
Why an "unpackaged" WinUI app packages cleanly
main calls windows_reactor::bootstrap(), which runs MddBootstrapInitialize2 with
OnPackageIdentity_NOOP (crates/libs/reactor/src/bootstrap.rs), so under MSIX package
identity the App SDK bootstrapper is a no-op and the runtime is resolved from the manifest's
<PackageDependency> 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:
vX.Y.Ztag →X.Y.Z.0(THE release; any-rc/+metasuffix is dropped for MSIX). Published to the stablelatest/alias and attached to the unified Gitea Release.mainpush /workflow_dispatch→X.<Y+1>.<run_number>.0(canary — the minor after the latestv*tag, perscripts/ci/pf-version.ps1, climbing by run number;canary/alias).
Signing & install
CI signs every build with Azure Artifact Signing (formerly Trusted Signing) — account
unomsigning, certificate profile unom-io, endpoint https://neu.codesigning.azure.net/. That
chain is publicly trusted, so there is nothing to import:
# install the package for your CPU (and re-run for each upgrade)
Add-AppxPackage -Path .\punktfunk-client-windows_<ver>_x64.msix # Intel/AMD
Add-AppxPackage -Path .\punktfunk-client-windows_<ver>_arm64.msix # ARM64 (Snapdragon, etc.)
The MSIX declares a dependency on the Windows App SDK 2.x runtime; install
the App SDK runtime if Add-AppxPackage reports a missing
Microsoft.WindowsAppRuntime.2 framework.
How signing resolves
pack-msix.ps1 picks a backend in this order:
- Azure Artifact Signing when
AZURE_CODESIGNING_ENDPOINT/_ACCOUNT/_PROFILEare all set (the workflow sets them; they aren't secret). Credentials come fromAZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRET— thepunktfunk-ci-signingservice principal, which holds only the Artifact Signing Certificate Profile Signer role scoped to theunom-ioprofile. Keys are HSM-backed and never leave Azure, so there is no.pfxand no.ceris emitted. MSIX_CERT_PFX_B64/MSIX_CERT_PASSWORD— the older stable self-signed cert (CN=unom, public half checked in aspunktfunk-codesign.cer), kept as a fallback.- An ephemeral self-signed cert (forks / local builds with no secrets at all).
Modes 2 and 3 still export a .cer to import into Cert:\LocalMachine\TrustedPeople first. On a
v* tag, a build with no real signing backend fails closed rather than shipping a throwaway.
Two things about Azure mode that are easy to get wrong:
- Timestamping is mandatory, not best-effort. Azure mints a leaf cert per request that expires in about three days. An untimestamped signature therefore stops verifying within days of release, so the script refuses to retry without one (modes 2 and 3 keep the old best-effort retry).
- The manifest
Publishermust equal the signer's subject exactly, because MSIX package identity is Name + Publisher. The default-Publisheris theunom-ioprofile's verified subject; after signing, the script reads the signature back off the.msixand fails the build on any drift. Changing it makes a different package — existing installs must be uninstalled, not upgraded.
Building locally
On the Windows runner / dev VM (MSVC + Windows SDK present), after a release build:
# 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\x86_64-pc-windows-msvc\release -OutDir C:\t\msix
# arm64 (cross-compiled; no extra environment — the client links no FFmpeg)
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
resolution). The only step that needs a real display is launching the WinUI window (same
on-glass constraint as the rest of the client).