Windows build¶
The contract iiiris maintains for first-class Windows support:
which artifacts ship, what the installer guarantees, how the
service-mode runtime behaves, and the build pipeline that produces
all of it. This spec replaces docs/briefs/WINDOWS_BUILD.md now
that Phases 3a + 3b + 3c have shipped end-to-end.
The operator-facing reference lives in
../deployment.md — install walkthroughs,
upgrade procedures, service-mode operations. This spec captures
what the implementation guarantees; that file captures how to
use it.
Audiences and artifacts¶
Three audiences, three artifacts. Each tagged release publishes all three.
| Artifact | Audience | Form |
|---|---|---|
| Source build | Contributors, self-builders | make build from an MSYS2 UCRT64 shell (see ../deployment.md). |
iiirisd-vX.Y.Z-windows-amd64.zip |
Operators comfortable extracting a zip | iiirisd.exe + ~90 MSYS2 UCRT64 DLLs + LICENSE/NOTICE/THIRD_PARTY_LICENSES.md. Extract and run. |
iiirisd-vX.Y.Z-windows-amd64.msi |
Production deployers | WiX v5 MSI. Per-user or all-users layout. Optional service-install mode with Event Log + rolling file logging. Self-signed. |
Build pipeline contract¶
- Toolchain: MSYS2 UCRT64.
mingw-w64-ucrt-x86_64-libvips,mingw-w64-ucrt-x86_64-gcc,mingw-w64-ucrt-x86_64-pkg-config,mingw-w64-ucrt-x86_64-openjpeg2.govips(CGO) is the only reason CGO is needed; the build is otherwise pure Go. Pinned by the Phase 1.1 spike at Go 1.25.0 / gcc 16.1.0 / libvips 8.18.2. - Runner: GitLab SaaS Windows pool (
saas-windows-medium-amd64). Cross-compiling CGO + libvips from Linux is impractical; Windows builds happen on Windows. - Linkage: dynamic.
iiirisd.exedepends on ~90 MSYS2 UCRT64 DLLs at runtime; the zip and MSI both bundle them alongside the binary. LGPL §6(d) is satisfied trivially by drop-in replacement of the libvips DLL. - DLL inventory:
deploy/installer/dlls.wxsis generated from the staged binary'slddclosure byscripts/wix-generate-dlls.ps1. The committed file is checked for staleness in CI (-Mode Check); MSYS2 dep drift fails the build with an actionable diff. Reviewer regenerates +commits before the build can pass. - CI cadence:
build-windows-amd64(zip) runs on every push;build-windows-msi(MSI) runs tag-only via the<<: *release-rulesanchor —.wxschanges infrequently and SaaS Windows minutes are budgeted accordingly. Both jobs consume the same staging directory artifact, so the MSI bundles exactly the same binary + DLL set the zip ships.
MSI installer contract¶
Layout¶
- Single MSI, two install scopes (WiX single-package
authoring via
ALLUSERS=2 MSIINSTALLPERUSER=…): - All-users — binary at
C:\Program Files\iiiris\, data atC:\ProgramData\iiiris\{config,cache,logs}\. Per-machine, requires Administrator. Default. - Per-user — binary at
%LocalAppData%\iiiris\, data still at%ProgramData%\iiiris\. No admin needed. Service-install mode is unavailable (HKLM writes required). - Layout chosen via
WixUI_Advanced'sWhichUsersDlg(per-machine default; per-user requires explicit user selection).
Default config¶
config.yaml.example(commented Windows-aware template) ships ininstaller/and lands at%ProgramData%\iiiris\config\config.yamlon first install only.NeverOverwrite="yes"+Permanent="yes"together mean: the example never replaces operator edits, and uninstall preserves the (possibly-edited) config.
Data dirs¶
%ProgramData%\iiiris\{config,cache,logs}\ are created on
install with Permanent="yes" on each Component. Uninstall
preserves all three. Operators wanting a clean wipe remove
%ProgramData%\iiiris\ manually after uninstall.
Service mode (optional)¶
- Enabled via
msiexec /i ... INSTALLSERVICE=1(or the future GUI checkbox). Default is binary-install mode. - Service name:
iiiris(matches theserviceNameconst incmd/iiirisd/service_windows.go). - Display name: "iiiris IIIF Image Server".
- Account:
NT AUTHORITY\LocalSystemby default. Operators can swap viaservices.msc→ Log On tab (NetworkService for least-privilege, or a dedicated domain account). - Start: auto on boot.
- Service arguments:
-config "%ProgramData%\iiiris\config\config.yaml"(resolved at install time via WiX's[CommonAppDataFolder]token). - ServiceControl:
Start="install"Stop="both"Remove="uninstall"Wait="yes". Stop-on-both handles the upgrade dance: stop old → replace binary → start new.
Event Log source¶
The installer writes two REG values under
HKLM\SYSTEM\CurrentControlSet\Services\EventLog\Application\iiiris
when service-install is enabled:
EventMessageFile(REG_EXPAND_SZ) = path of the installediiirisd.exe. No embedded message table; Windows logs records verbatim.TypesSupported(REG_DWORD) =7(Info | Warning | Error).
internal/observability.EventLogHandler opens this source at
runtime. If the source is absent (pre-3c installs, manual
sc.exe registrations, per-user installs that can't write
HKLM), the handler returns nil and falls back to file +
stderr — graceful degradation by design.
Upgrade¶
MajorUpgrade Schedule="afterInstallExecute":
- New install begins.
ServiceControl Stop="both"stops the running service.InstallFilesreplacesiiirisd.exe+ bundled DLLs (no file-lock contention because the service is stopped).ServiceControl Start="install"starts the new binary.RemoveExistingProductsremoves the old MSI's product entry; same-Guid components are no-ops.
Brief downtime window (seconds). Single-host install assumption; no rolling-upgrade pattern.
Uninstall¶
Removes the binary, bundled DLLs, Start Menu shortcuts, service
registration (when present), Event Log source registry keys.
Preserves all Permanent="yes" components:
%ProgramData%\iiiris\{config,cache,logs}\ plus the operator's
config.yaml. Manual Remove-Item -Recurse "%ProgramData%\iiiris"
is the clean-wipe escape hatch.
Start Menu shortcuts¶
Three entries under Start Menu → iiiris\:
- "iiiris IIIF Server" — console-mode launch. Binary-install only (hidden in service mode via Component Condition; the service already binds :8080).
- "Open iiiris admin" — Internet shortcut to
http://localhost:8080/admin/. Always present; useful onceIIIRIS_ADMIN_USER/IIIRIS_ADMIN_PASSare set. - "Uninstall iiiris" —
msiexec /x [ProductCode]. Always present.
Code signing¶
- Self-signed for the first cut. A future EV/OV cert
procurement is a drop-in replacement (same CI secrets, the cert
just changes;
SIGN_MSI=1stays). - SmartScreen shows "Unknown publisher" on first run; users click-through via "More info → Run anyway" or right-click → Properties → Unblock. The self-signature proves the artifact wasn't tampered after CI built it.
- CI gate:
SIGN_MSIvariable, default"0". When"1",scripts/sign-msi.ps1runssigntool sign /f <pfx> /p <pass> /tr http://timestamp.digicert.com /td sha256 /fd sha256and verifies viasigntool verify /pa.
Runtime contract (Windows service mode)¶
Phase 3a's Go-side surface in cmd/iiirisd/:
- SCM context detection:
isWindowsService()(viagolang.org/x/sys/windows/svc.IsWindowsService) returns true when launched by SCM. Errors from the underlying API are treated as "not a service" so the binary falls back to console mode rather than wedging. - Service handler:
runService(configPath)reusessetup(cfg, configPath, extras...)andserve(bundle, stop)from the console path. Extras = Event Log handler + rolling-file handler. The handler translates SCMStop/Shutdowncontrol events into a stop-channel close thatserve()blocks on. Exit code flows back to SCM. - Build-tagged files:
service_{windows,unix}.go,is_service_{windows,unix}.go,signals_{windows,unix}.go. Non-Windows stubs keep the package buildable; runtime dispatch never reaches them.
Portability fixes (Phase 1)¶
Universal cross-platform corrections that landed during Phase 1 and remain part of the contract:
signals_unix.go/signals_windows.go—SIGTERMis Unix-only; the Windows path usesos.Interrupt. Build-tagged helpershutdownSignals()returns the right set per OS.source.safePathrejects on every platform:- NUL bytes (defense-in-depth).
- Colon
:(Windows drive-letter injection + NTFS alternate data streams). - Reserved DOS device names (
CON,PRN,AUX,NUL,COM0-9,LPT0-9) in any path component, stem-matched with trailing dot/space stripped. Applies universally so Linux-authored collections stay portable. cache.Filesystem.Putnotes the Windows rename sharing-violation edge (open file during rename); the actual retry wrapper lands if realistic-load smoke surfaces the failure mode.
Smoke test contract¶
scripts/smoke-msi.ps1 runs after every build-windows-msi
build. Two subtests:
- Binary-mode — install (
msiexec /qn ALLUSERS=2), run installed binary,/health, one IIIF render, uninstall, assert data preserved. - Service-mode — install (
/qn ALLUSERS=2 INSTALLSERVICE=1), assertGet-Service iiirisreturns a registered service + Event Log source registry keys exist with the rightTypesSupported/EventMessageFile, poll/healthon :8080,net stop iiiris, uninstall, assert service deregistered + Event Log keys gone + data still preserved.
The job's CI artifacts include both install/uninstall logs, the installed binary's stdio, the service log file, and the rendered JPEG — enough to diagnose any failure without reproducing locally.
A separate scripts/smoke-windows.ps1 (per-format + console +
service-via-sc.exe lifecycle) runs after build-windows-amd64
and covers the staged-binary surface independent of the MSI.
Out of scope¶
- Cross-compiling Windows from Linux. CGO + libvips on mingw-on-Linux is a tarpit; the project explicitly opted out. Windows builds happen on Windows.
- macOS installer. Source build only; no published artifact.
- Multi-architecture Windows.
windows/amd64only.windows/arm64lands if demand materialises. - Auto-update / update channels. Each MSI is a manual
download.
MajorUpgradehandles the install-over-install case; nothing phones home. - In-installer GUI checkbox for service mode. Currently
command-line-driven (
INSTALLSERVICE=1). Custom dialog page is a polish follow-up. - PerfMon / ETW tracing. No Windows-specific telemetry beyond what slog + Event Log already provide.
- HKEY_LOCAL_MACHINE writes outside the service path. The
installer writes only what's needed for service registration +
Event Log source. No per-app
HKLM\SOFTWARE\iiiriskeys outside the synthetic component KeyPath.