# Build the punktfunk Windows HOST as a signed Inno Setup installer and publish it to Gitea's generic # package registry, so a Windows GPU box can install the streaming host (SYSTEM service + bundled # pf-vdisplay virtual-display driver + the web management console, run by a scheduled task on a bundled # bun) from one signed setup.exe. Runs on the self-hosted Windows runner # (host mode; scripts/ci/setup-windows-runner.ps1) — same MSVC/Windows-SDK/LLVM env as windows.yml. # # Why an installer and not MSIX (like the client): the host installs a LocalSystem SCM service that # CreateProcessAsUserW's into the interactive session for secure-desktop capture, and bundles a # kernel/IDD driver — neither is expressible in MSIX's sandbox. The real install logic already lives # in `punktfunk-host service install` (crates/punktfunk-host/src/service.rs); the installer just lays # the exe down and calls it elevated. Packaging internals: packaging/windows/README.md. # # Registry (public reads, unom org): https://git.unom.io/unom/-/packages (generic group) # # Versioning (free-form; not MSIX's 4-part rule) — single project version: # vX.Y.Z tag -> X.Y.Z (THE release; published + stable `latest/` alias + attached to the # unified Gitea Release). # main push / dispatch -> 0.3. (canary; `canary/` alias; climbs by run number). # # Signing reuses the client's MSIX_CERT_PFX_B64 / MSIX_CERT_PASSWORD secrets (CN=unom). Without them # an ephemeral self-signed cert is generated and its public .cer published next to the installer # (import once to LocalMachine\TrustedPublisher). See packaging/windows/pack-host-installer.ps1. # # GPU backends: the host builds with --features nvenc,amf-qsv = all three vendors in one installer. # - NVENC (NVIDIA, direct SDK): the only link need is nvencodeapi.lib, synthesised from a 2-export # .def with llvm-dlltool (no GPU/SDK at build time). # - AMF/QSV (AMD/Intel, libavcodec): link-imports the FFmpeg libs from FFMPEG_DIR (the BtbN gpl-shared # tree the client uses; includes the *_amf/*_qsv encoders) and bundles its DLLs into the installer. # CI never launches the exe, so no GPU is needed here — this is build + Windows clippy coverage only. name: windows-host on: push: branches: [main] paths: - 'crates/punktfunk-host/**' - 'crates/punktfunk-core/**' - 'packaging/windows/**' - 'scripts/windows/**' - 'web/**' - 'Cargo.lock' - 'Cargo.toml' - '.gitea/workflows/windows-host.yml' tags: ['v*'] workflow_dispatch: env: REGISTRY: git.unom.io OWNER: unom PKG: punktfunk-host-windows jobs: package: runs-on: windows-amd64 timeout-minutes: 90 steps: - uses: actions/checkout@v4 - name: Locale-safety gate (installer-run scripts must be ASCII) shell: pwsh # The installer runs these via powershell.exe (Windows PowerShell 5.1) and cmd.exe on the END # USER's box. PS 5.1 reads a BOM-less script in the active ANSI codepage, so on a non-UTF-8 locale # (e.g. German Windows-1252) a stray em-dash mis-decodes into a curly quote and the script aborts # with "unterminated string" - exactly how the pf-vdisplay driver install silently failed in the # field. Keep every installer-run script pure ASCII (matches install-gamepad-drivers.ps1). run: | $bad = Get-ChildItem packaging/windows/*.ps1, scripts/windows/*.ps1, scripts/windows/*.cmd -ErrorAction SilentlyContinue | Where-Object { [IO.File]::ReadAllText($_.FullName) -match '[^\x00-\x7F]' } if ($bad) { $bad.FullName | ForEach-Object { Write-Output "::error::non-ASCII in installer-run script: $_" } throw "installer-run scripts must be pure ASCII (PS 5.1 mis-parses them on non-UTF-8 locales)" } Write-Output "installer-run scripts are ASCII-clean" - name: Configure + version shell: pwsh run: | # CARGO_TARGET_DIR=C:\t dodges the MAX_PATH wall in the CMake-from-source crates (aws-lc, # opus) the host pulls; CARGO_WORKSPACE_DIR mirrors the client workflows. Both via GITHUB_ENV # (pwsh Out-File utf8 = no BOM, unlike Windows PowerShell 5.1 — keeps the first line clean). "CARGO_TARGET_DIR=C:\t" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "CARGO_WORKSPACE_DIR=$env:GITHUB_WORKSPACE" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 # FFMPEG_DIR: the same BtbN gpl-shared x64 tree the Windows CLIENT links against (provisioned # by scripts/ci/setup-windows-runner.ps1). The host's AMD/Intel AMF/QSV encode backend # (--features amf-qsv) link-imports avcodec/avutil/swscale from it; pack-host-installer.ps1 # then bundles its bin\*.dll into the installer. LIBCLANG_PATH is in the runner daemon env. if (-not $env:FFMPEG_DIR) { "FFMPEG_DIR=C:\Users\Public\ffmpeg" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 } $v = if ($env:GITHUB_REF -like 'refs/tags/v*') { $env:GITHUB_REF_NAME -replace '^v', '' } else { "0.3.$($env:GITHUB_RUN_NUMBER)" } "HOST_VERSION=$v" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "PUNKTFUNK_BUILD_VERSION=$v" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 Write-Output "host version $v" - name: Generate NVENC import lib shell: pwsh run: | & packaging/windows/nvenc/gen-nvenc-importlib.ps1 -OutDir C:\t\nvenc "PUNKTFUNK_NVENC_LIB_DIR=C:\t\nvenc" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Build (release, nvenc + amf-qsv) shell: pwsh # All-vendor host: NVENC (NVIDIA, direct SDK) + AMF/QSV (AMD/Intel, libavcodec via FFMPEG_DIR). run: cargo build --release -p punktfunk-host --features nvenc,amf-qsv - name: Clippy (host, Windows) shell: pwsh # First-ever Windows lint coverage for the host (Linux CI never lints the windows-cfg code). run: cargo clippy -p punktfunk-host --features nvenc,amf-qsv -- -D warnings - name: Build + lint the HDR Vulkan layer (pf-vkhdr-layer) shell: pwsh # Standalone cdylib (own [workspace]) the installer bundles + registers (it lets Vulkan games # like Doom use HDR on the virtual display). Lint here so a regression fails CI instead of # silently shipping the host without the layer (pack-host-installer.ps1 builds it non-fatally). # Windows-only FFI (user32 + the vk_layer loader glue) → can't be linted on the Linux CI. run: | Push-Location packaging/windows/pf-vkhdr-layer cargo fmt --check; if ($LASTEXITCODE) { throw "pf-vkhdr-layer rustfmt" } cargo clippy --release -- -D warnings; if ($LASTEXITCODE) { throw "pf-vkhdr-layer clippy" } Pop-Location - name: Ensure Inno Setup shell: pwsh run: | if (-not (Test-Path 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe') -and -not (Get-Command iscc -ErrorAction SilentlyContinue)) { Write-Output "installing Inno Setup via choco" choco install innosetup -y --no-progress } - name: Fetch portable bun runtime (build tool + bundled to run the console) shell: pwsh run: | # ONE pinned bun, used both to BUILD the console and shipped in the installer to RUN it. The # .output is self-contained (Nitro noExternals — deps bundled + tree-shaken, no node_modules), # so the installer ships just bun + a ~75-file .output instead of node + a node_modules forest. $ver = 'bun-v1.3.14' $url = "https://github.com/oven-sh/bun/releases/download/$ver/bun-windows-x64.zip" New-Item -ItemType Directory -Force -Path C:\t | Out-Null $zip = 'C:\t\bun.zip'; $dst = 'C:\t\bundist' Invoke-WebRequest -Uri $url -OutFile $zip if (Test-Path $dst) { Remove-Item $dst -Recurse -Force } Expand-Archive -Path $zip -DestinationPath $dst -Force $bun = (Get-ChildItem -Path $dst -Recurse -Filter bun.exe | Select-Object -First 1).FullName if (-not $bun) { throw "bun.exe not found in $url" } "BUN_EXE=$bun" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 & $bun --version - name: Build + smoke-boot web console (bun) shell: pwsh env: # PAT with read access to the unom org packages — the @unom npm registry needs auth to BUILD. REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} # The bun fetched above builds the Nitro server AND runs it. noExternals (vite.config) makes the # output self-contained, so there's no .output/server install — the installer ships bun + the # ~75-file .output. The runner is SYSTEM with no ~/.npmrc, so supply the private @unom token in # the SYSTEM home .npmrc to BUILD (kept OUT of the shipped bundle — web\.npmrc has only the # registry mapping, and nothing copies it into .output). run: | $bun = $env:BUN_EXE if ($env:REGISTRY_TOKEN) { $rc = Join-Path $env:USERPROFILE '.npmrc' Add-Content -Path $rc -Value '@unom:registry=https://git.unom.io/api/packages/unom/npm/' Add-Content -Path $rc -Value "//git.unom.io/api/packages/unom/npm/:_authToken=$env:REGISTRY_TOKEN" } Push-Location web & $bun install --frozen-lockfile; if ($LASTEXITCODE) { throw "bun install failed ($LASTEXITCODE)" } & $bun run build; if ($LASTEXITCODE) { throw "web build failed ($LASTEXITCODE)" } if (Select-String -Path .output\server\index.mjs -Pattern 'Bun\.serve' -Quiet) { throw "web build is a bun bundle (Bun.serve) - need the node-server preset" } Pop-Location # Gate the installer on a real boot under the BUNDLED bun (the runtime it ships), serving /login. $env:PORT = '3009'; $env:HOST = '127.0.0.1'; $env:PUNKTFUNK_UI_PASSWORD = 'ci' $server = (Resolve-Path 'web\.output\server\index.mjs').Path $p = Start-Process -FilePath $bun -ArgumentList $server -PassThru -WindowStyle Hidden Start-Sleep -Seconds 4 try { $code = (Invoke-WebRequest -Uri 'http://127.0.0.1:3009/login' -UseBasicParsing -TimeoutSec 10).StatusCode } catch { $code = 0 } Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue Write-Output "web console smoke (bun): /login -> $code" if ($code -ne 200) { throw "web console failed to boot under bun" } "WEB_OUTPUT_DIR=$((Resolve-Path 'web\.output').Path)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Pack + sign installer shell: pwsh env: MSIX_CERT_PFX_B64: ${{ secrets.MSIX_CERT_PFX_B64 }} MSIX_CERT_PASSWORD: ${{ secrets.MSIX_CERT_PASSWORD }} run: | & packaging/windows/pack-host-installer.ps1 ` -Version $env:HOST_VERSION -TargetDir C:\t\release -OutDir C:\t\out - name: Publish to Gitea generic registry shell: pwsh env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: | # Check curl's exit code ourselves — a best-effort DELETE (404 on first run) must not abort. $PSNativeCommandUseErrorActionPreference = $false function Publish-File($f, $url) { curl.exe -fsS --user "enricobuehler:$($env:REGISTRY_TOKEN)" --upload-file "$f" "$url" if ($LASTEXITCODE -ne 0) { throw "upload failed ($LASTEXITCODE): $url" } Write-Output "published $url" } $files = @($env:HOST_SETUP_PATH, $env:HOST_CER_PATH) | Where-Object { $_ -and (Test-Path $_) } if (-not $files) { throw "pack produced no artifacts to publish" } $base = "https://$($env:REGISTRY)/api/packages/$($env:OWNER)/generic/$($env:PKG)" foreach ($f in $files) { Publish-File $f "$base/$($env:HOST_VERSION)/$(Split-Path $f -Leaf)" } # Refresh the channel alias (delete-then-reupload, like flatpak.yml/decky.yml) for a # predictable download URL: stable release -> `latest/`, canary main build -> `canary/`. $alias = if ($env:GITHUB_REF -like 'refs/tags/v*') { 'latest' } else { 'canary' } $aliasNames = @{ $env:HOST_SETUP_PATH = 'punktfunk-host-setup.exe'; $env:HOST_CER_PATH = 'punktfunk-host-windows.cer' } foreach ($f in $files) { $an = $aliasNames[$f]; if (-not $an) { continue } curl.exe -fsS -o NUL --user "enricobuehler:$($env:REGISTRY_TOKEN)" -X DELETE "$base/$alias/$an" 2>$null Publish-File $f "$base/$alias/$an" } # On a real release, also attach the signed installer (+ its .cer) to the unified Gitea Release. - name: Attach host installer to the Gitea release (stable tags only) if: startsWith(gitea.ref, 'refs/tags/v') shell: pwsh env: GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: | . scripts/ci/gitea-release.ps1 $rid = Ensure-GiteaRelease -Tag $env:GITHUB_REF_NAME -Name $env:GITHUB_REF_NAME -Prerelease 'auto' foreach ($f in @($env:HOST_SETUP_PATH, $env:HOST_CER_PATH)) { if ($f -and (Test-Path $f)) { Upsert-GiteaAsset -ReleaseId $rid -File $f } }