Krill Desktop built .deb (Linux) and .dmg (macOS) artifacts, and the Compose
nativeDistributions block listed TargetFormat.Msi with packageName/vendor
already set — but no pipeline ever produced a Windows installer, so Windows users
had nothing to download.
The gap was infrastructure, not application code: jpackage can only emit a .msi
from a Windows host, and there was no CI job running on a Windows runner. Windows
desktop is the same jvm("desktop") target as Linux/macOS, so no new Kotlin
target or expect/actual was needed — only a Windows build host, a little
packaging metadata (icon, upgrade GUID, Start-menu group), and a publish
workflow.
composeApp/build.gradle.kts: add a windows { } block under
nativeDistributions — iconFile → a new src/desktopMain/resources/icon.ico
(generated from the same 1024×1024 source art as the macOS .icns), a stable
upgradeUuid (30894E47-…) so newer installers upgrade in place rather than
side-by-side, menuGroup = "Krill", and perUserInstall = true (no admin
elevation). packageVersion already reads version.txt via desktopPackageVersion,
so the in-MSI version tracks the release for free. New
.github/workflows/Deploy Windows.yml runs on the GitHub-hosted windows-latest
runner, installs the WiX Toolset v3 (jpackage’s MSI backend, no longer
preinstalled on the image), builds :composeApp:packageReleaseMsi, and publishes
versioned + latest keys to s3://cms.krill.systems/distro/windows/ with a
CloudFront invalidation (E1MKRDHOFUF59Y, the same distribution the macOS/
screenshot/SDK-docs workflows use). A signtool Authenticode step is gated on a
WINDOWS_SIGNING_CERT secret — v1 ships unsigned. A Download-category post
documents the link and the SmartScreen “More info → Run anyway” steps.
TargetFormat ships nothing on its own. When a cross-platform
desktop target advertises an installer format, confirm a build host for that
OS actually exists in CI before assuming users can download it — the format
being enabled does not mean anything is produced. (Same lesson as the macOS
.dmg; .msi was listed for far longer with no pipeline.).msi was built —
so a GitHub-hosted runner is sufficient, and signing is an orthogonal,
secret-gated concern. Don’t conflate “official installer” with “self-hosted
build.”upgradeUuid must be generated once and frozen. Changing it later makes a
new .msi install side-by-side instead of upgrading, stranding the old
version on every existing machine. It is committed inline with a “never change
this” comment for exactly that reason.desktopMain surfaces were only ever proven on Linux/macOS. The
expect/actuals (AlertSound, Clipboard, ImageUtil, EmojiSupport,
ImagePicker, SvgFilePicker) and any path assumptions are unexercised on
Windows until the app actually runs there — tracked as a manual smoke
checklist on the implementing PR, not something the Linux/macOS builds catch.The build config, workflow, icon, and docs were authored on Linux. The following require a Windows machine and are checklisted on the PR for manual completion before the first publish is trusted:
packageReleaseMsi build (with WiX installed) producing a .msi, and
recording whether the ProGuard release variant worked or a fallback to
packageMsi was needed (mirrors the macOS ProGuard concern, which did not
materialize there).desktopMain actual on Windows, plus
~/.krill/ path resolution (the macOS audit found a dormant installId()
mkdir-ordering fragility in shared/ — re-check it holds on Windows paths).upgradeUuid.Deploy Windows.yml, confirm the versioned and latest
CloudFront URLs both serve the .msi.