Files
punktfunk/crates/punktfunk-client-windows/packaging
enricobuehler f44317fb33
apple / swift (push) Successful in 54s
windows-msix / package (push) Successful in 1m0s
windows / build (push) Successful in 55s
android / android (push) Failing after 56s
ci / web (push) Successful in 32s
ci / docs-site (push) Successful in 39s
ci / rust (push) Failing after 3m21s
deb / build-publish (push) Successful in 2m5s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
ci / bench (push) Successful in 4m45s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 5m27s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 5m28s
docker / deploy-docs (push) Successful in 10s
feat(windows): stable code-signing cert for the MSIX (one-time per-machine trust)
Sign every MSIX build with one STABLE self-signed cert instead of a fresh per-build cert, so the
Trusted People import is a one-time, per-machine step that survives upgrades (a fresh cert each build
forced a re-import every time). The cert (CN=unom, SHA-1 CD1EFDEE…E941, valid to 2036) lives in the
MSIX_CERT_PFX_B64 / MSIX_CERT_PASSWORD Actions secrets; its public half is checked in as
packaging/punktfunk-codesign.cer and published next to each .msix.

pack-msix.ps1 now always exports the signing cert's public .cer (extracted from a supplied pfx too,
not just the ephemeral-generated path) and warns if the cert subject != manifest Publisher (the
mismatch Add-AppxPackage would otherwise reject). Documents the path to a publicly-trusted
(no-import) cert: swap the two secrets + pass a matching -Publisher.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:17:30 +00:00
..

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 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 <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:

  • 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_dispatch0.2.<run_number>.0 (rolling, climbs by run number).

Signing & install

CI signs every build with a stable self-signed code-signing cert (CN=unom, SHA-1 CD1EFDEEEC9743AFC38F56C5AF30C5A3009BE941, valid to 2036). Its public half is checked in as punktfunk-codesign.cer; the private .pfx + password live in the MSIX_CERT_PFX_B64 / MSIX_CERT_PASSWORD Actions secrets. Because it's the same cert every build, trusting it is one-time, per machine — once imported, every future build and in-place upgrade is trusted with no further prompt:

# 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_<ver>_x64.msix

The matching .cer is also published next to each .msix in the registry, so it's always at hand.

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.

pack-msix.ps1 signing precedence: it uses the MSIX_CERT_PFX_B64 / MSIX_CERT_PASSWORD secrets when present (the stable cert above), else generates an ephemeral self-signed cert (forks / local builds without the secrets). Either way it exports the signing cert's public .cer for the import. To move to a publicly-trusted (no-import) cert — Azure Artifact Signing or a public OV cert — replace the two secrets with the new .pfx; the cert's subject DN must equal the manifest Publisher, so pass a matching -Publisher (it's stamped into the package Identity, and changing it changes the package identity → a one-time reinstall).

Building locally

On the Windows runner / dev VM (MSVC + Windows SDK present), after a release build:

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).