feat(packaging): signed Inno Setup installer for the Windows host + CI
apple / swift (push) Successful in 54s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
windows-host / package (push) Failing after 6m18s
android / android (push) Failing after 2m12s
ci / web (push) Successful in 38s
ci / rust (push) Failing after 1m40s
ci / docs-site (push) Successful in 29s
deb / build-publish (push) Successful in 2m35s
decky / build-publish (push) Successful in 24s
ci / bench (push) Successful in 4m32s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 3m35s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 20s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m33s
docker / deploy-docs (push) Successful in 22s

MSIX (the client's format) can't install the host's LocalSystem secure-desktop
service or the SudoVDA kernel driver, so the host ships as a signed Inno Setup
setup.exe that runs elevated and delegates to the existing idempotent
`punktfunk-host service install`.

- packaging/windows/punktfunk-host.iss: lay exe into Program Files, optional
  SudoVDA driver task, run service install/start; [Code] stops+waits the service
  before file copy on upgrade; uninstall runs service uninstall.
- pack-host-installer.ps1: cert (reuses MSIX_CERT_PFX_B64 / self-signed CN=unom),
  sign inner exe + setup.exe, fetch/stage SudoVDA, run ISCC, export public .cer.
- fetch-sudovda.ps1 / install-sudovda.ps1: pinned SudoVDA + nefcon download, cert
  import, gated device-node create (no phantom dup), pnputil install (warn-not-abort).
- nvenc/: synthesize nvencodeapi.lib via llvm-dlltool from a 2-export .def so
  --features nvenc links with no GPU/SDK at build time.
- .gitea/workflows/windows-host.yml: build (nvenc) -> clippy -> ISCC -> sign ->
  publish setup.exe + .cer to the generic registry pkg punktfunk-host-windows.
  Tag host-win-v* -> X.Y.Z (+ latest/ alias); main push -> rolling 0.2.<run>.
- setup-windows-runner.ps1: provision Inno Setup; docs: installer instructions.

SudoVDA/nefcon release URLs+SHA-256s in fetch-sudovda.ps1 are placeholders
(baseline v0.2.1) — fetch warns + prints the computed hash until pinned.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 23:05:20 +00:00
parent f4cff765ed
commit 16d3b7767e
11 changed files with 733 additions and 2 deletions
+73
View File
@@ -0,0 +1,73 @@
# Windows host packaging — signed Inno Setup installer
A one-file, signed `setup.exe` for the punktfunk streaming **host** on Windows, published to Gitea's
generic package registry (`punktfunk-host-windows`) by `.gitea/workflows/windows-host.yml`.
## Why not MSIX (like the client)
The host installs a **`LocalSystem` SCM service** that `CreateProcessAsUserW`'s from Session 0 into the
interactive session for secure-desktop (UAC / lock screen) capture, adds firewall rules, and depends
on the **SudoVDA** kernel/IDD virtual-display driver. MSIX's sandbox can install **neither** a SYSTEM
service of this kind **nor** a driver. So the host ships as a classic elevated installer.
The installer is deliberately thin: the real install logic — SCM registration, firewall rules, the
default `host.env`, and the SYSTEM→interactive-session supervisor — already lives in
`punktfunk-host service install` (`crates/punktfunk-host/src/service.rs`). The installer just lays the
exe into `C:\Program Files\punktfunk\` and calls that subcommand, elevated.
## What the installer does
- Installs `punktfunk-host.exe` (+ `host.env.example`, this README) to `{app}` (`C:\Program Files\punktfunk`).
- **Optional task** *Install the SudoVDA virtual display driver* — imports the driver's self-signed
cert (machine `Root` + `TrustedPublisher`), creates the `root\sudomaker\sudovda` device node (only
if absent — `install-sudovda.ps1`), and stages the driver with `pnputil /add-driver /install`.
Best-effort: a driver failure warns but never aborts the install (the host degrades to a physical
display without it).
- Runs `punktfunk-host service install` (idempotent; writes a default `host.env` only if absent, so
user config survives upgrades) and, by the *Start service now* task, `service start`.
- **Upgrade:** stops a running `PunktfunkHost` service and waits for `STOPPED` before replacing files
(otherwise the locked exe / respawning supervisor would block the copy), then re-points the service.
- **Uninstall** (Add/Remove Programs): runs `service uninstall` (stop + delete service + remove
firewall rules). The SudoVDA driver is intentionally left installed.
Silent install: `punktfunk-host-setup-<ver>.exe /VERYSILENT` (omit the driver with `/MERGETASKS="!installdriver"`).
## Prerequisites on the target box
- An **NVIDIA GPU + driver** — the installer's exe is built `--features nvenc` and load-depends on the
driver's `nvEncodeAPI64.dll`.
- **ViGEmBus** (optional) for virtual gamepads — still a manual prerequisite (not bundled yet):
<https://github.com/nefarius/ViGEmBus/releases>.
## Files here
| File | Role |
|------|------|
| `punktfunk-host.iss` | Inno Setup script (the installer definition). |
| `pack-host-installer.ps1` | Orchestrator: cert + sign, fetch/stage SudoVDA, run ISCC, sign setup.exe, emit registry paths. |
| `fetch-sudovda.ps1` | Download + SHA-256-verify the **pinned** SudoVDA + nefcon releases; stage the driver payload. |
| `install-sudovda.ps1` | Runs at install time (elevated): trust cert → gated device-node create → `pnputil` install. |
| `nvenc/nvenc.def`, `nvenc/gen-nvenc-importlib.ps1` | Synthesise `nvencodeapi.lib` for the `--features nvenc` link (llvm-dlltool / lib.exe). |
> **Pinning:** the SudoVDA / nefcon release URLs + SHA-256s in `fetch-sudovda.ps1` are the source of
> truth for what ships. Confirm the latest asset URLs and fill the SHA-256s to lock a release.
## Build locally (Windows, MSVC + Windows SDK + Inno Setup)
```powershell
# 1. import lib for the nvenc link
pwsh -File packaging\windows\nvenc\gen-nvenc-importlib.ps1 -OutDir C:\t\nvenc
$env:PUNKTFUNK_NVENC_LIB_DIR = 'C:\t\nvenc'
# 2. build the host
cargo build --release -p punktfunk-host --features nvenc
# 3. pack (self-signed unless MSIX_CERT_PFX_B64/MSIX_CERT_PASSWORD are set; -NoDriver to skip SudoVDA)
pwsh -File packaging\windows\pack-host-installer.ps1 -Version 0.0.0-dev -TargetDir C:\t\release -OutDir C:\t\out
```
## Release
Push a `host-win-vX.Y.Z` tag — the workflow builds, signs, and publishes
`punktfunk-host-setup-X.Y.Z.exe` + the public `.cer`, and refreshes the `latest/` alias. Main pushes
publish rolling `0.2.<run>` builds (no `latest/` update).
+91
View File
@@ -0,0 +1,91 @@
<#
.SYNOPSIS
Download + verify the SudoVDA virtual-display driver and the nefcon device tool, and stage the
files the installer bundles into -OutDir.
.DESCRIPTION
The host uses SudoVDA (the SudoMaker Indirect Display Driver the Apollo Sunshine-fork ships) for
client-native-resolution virtual outputs (crates/punktfunk-host/src/vdisplay/sudovda.rs). The
driver isn't in this repo; CI fetches a PINNED release at pack time so the installer can carry it.
nefcon (nefarius/nefcon) provides `nefconc.exe`, used to create the root-enumerated device node
(pnputil cannot create a software/root device node from nothing).
Output (consumed by punktfunk-host.iss): -OutDir gets the driver payload (*.inf/*.cat/*.dll/*.sys
+ the signing *.cer) and nefconc.exe, flattened. install-sudovda.ps1 is copied in by the packer.
PINNING: the URLs + SHA-256s below are the single source of truth for what ships. If a Sha256 is
left empty, the script downloads, prints the computed hash, and continues with a loud warning —
fill it in to lock the release. Override any field with the params for a one-off build.
.EXAMPLE
pwsh -File fetch-sudovda.ps1 -OutDir C:\t\out\stage
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$OutDir,
# --- PINNED SudoVDA release (https://github.com/SudoMaker/SudoVDA/releases) ---------------
# Baseline validated by the host backend: SudoVDA 0.2.1 (sudovda.rs header). Confirm the latest
# asset name/URL at impl time; fill Sha256 to lock it.
[string]$SudoVdaTag = 'v0.2.1',
[string]$SudoVdaUrl = 'https://github.com/SudoMaker/SudoVDA/releases/download/v0.2.1/SudoVDA-v0.2.1.zip',
[string]$SudoVdaSha256 = '',
# --- PINNED nefcon release (https://github.com/nefarius/nefcon/releases) ------------------
[string]$NefconUrl = 'https://github.com/nefarius/nefcon/releases/download/v1.0.2.0/nefconw.zip',
[string]$NefconSha256 = ''
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Get-Verified([string]$Url, [string]$ExpectedSha256, [string]$Dest) {
Write-Host "==> downloading $Url"
Invoke-WebRequest -Uri $Url -OutFile $Dest -UseBasicParsing
$got = (Get-FileHash -Algorithm SHA256 -Path $Dest).Hash.ToLowerInvariant()
if ($ExpectedSha256) {
if ($got -ne $ExpectedSha256.ToLowerInvariant()) {
throw "SHA-256 mismatch for $Url`n expected $ExpectedSha256`n got $got"
}
Write-Host " sha256 ok ($got)"
}
else {
Write-Warning "no pinned SHA-256 for $Url — computed $got (PIN THIS in fetch-sudovda.ps1)"
}
}
# fresh staging dir
if (Test-Path $OutDir) { Remove-Item -Recurse -Force $OutDir }
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
$work = Join-Path $OutDir '_work'
New-Item -ItemType Directory -Force -Path $work | Out-Null
# --- SudoVDA driver payload -------------------------------------------------------------------
$sudoZip = Join-Path $work 'sudovda.zip'
Get-Verified -Url $SudoVdaUrl -ExpectedSha256 $SudoVdaSha256 -Dest $sudoZip
$sudoEx = Join-Path $work 'sudovda'
Expand-Archive -Path $sudoZip -DestinationPath $sudoEx -Force
# Flatten the driver files we need into -OutDir. SudoVDA is a user-mode IDD (a .dll registered by an
# .inf); pull the .inf/.cat/.dll plus a .sys if present and the signing .cer.
$driverFiles = Get-ChildItem -Path $sudoEx -Recurse -Include *.inf, *.cat, *.dll, *.sys, *.cer
if (-not ($driverFiles | Where-Object Extension -eq '.inf')) {
throw "no .inf found in the SudoVDA archive ($SudoVdaUrl) — wrong asset?"
}
$driverFiles | ForEach-Object { Copy-Item $_.FullName (Join-Path $OutDir $_.Name) -Force }
# --- nefcon (nefconc.exe) ---------------------------------------------------------------------
$nefZip = Join-Path $work 'nefcon.zip'
Get-Verified -Url $NefconUrl -ExpectedSha256 $NefconSha256 -Dest $nefZip
$nefEx = Join-Path $work 'nefcon'
Expand-Archive -Path $nefZip -DestinationPath $nefEx -Force
# Prefer the x64 console build.
$nefc = Get-ChildItem -Path $nefEx -Recurse -Filter 'nefconc.exe' |
Where-Object { $_.FullName -match '(?i)(x64|amd64)' } |
Select-Object -First 1
if (-not $nefc) { $nefc = Get-ChildItem -Path $nefEx -Recurse -Filter 'nefconc.exe' | Select-Object -First 1 }
if (-not $nefc) { throw "nefconc.exe not found in $NefconUrl" }
Copy-Item $nefc.FullName (Join-Path $OutDir 'nefconc.exe') -Force
Remove-Item -Recurse -Force $work
Write-Host "==> staged SudoVDA + nefcon in $OutDir :"
Get-ChildItem $OutDir -File | ForEach-Object { " $($_.Name)" }
+78
View File
@@ -0,0 +1,78 @@
<#
.SYNOPSIS
Install the bundled SudoVDA virtual-display driver. Runs ELEVATED at setup time (invoked from the
installer's [Run] section). Best-effort: warns and exits 0 on any failure — the host degrades to a
physical display without SudoVDA, so a driver hiccup must never abort the whole install.
.DESCRIPTION
-Dir holds the staged payload from fetch-sudovda.ps1 (the .inf/.cat/.dll + signing .cer + nefconc.exe).
Steps:
1. Trust the self-signed driver cert (machine Root + TrustedPublisher) so PnP installs it silently.
2. Create the root device node IF ABSENT (gated — a blind re-create spawns a phantom duplicate, and
the host's open_device() binds interface index 0; crates/punktfunk-host/src/vdisplay/sudovda.rs).
3. Stage + bind the driver (pnputil /add-driver /install — modern, in-box, idempotent).
Class/ClassGuid are read from the .inf so they always match the shipped driver.
.EXAMPLE
powershell -ExecutionPolicy Bypass -File install-sudovda.ps1 -Dir C:\path\to\sudovda
#>
[CmdletBinding()]
param(
[string]$Dir = $PSScriptRoot,
[string]$HardwareId = 'root\sudomaker\sudovda' # verified live (docs/windows-host.md)
)
# Never abort the installer on a driver failure.
$ErrorActionPreference = 'Continue'
trap { Write-Warning "SudoVDA install error: $_"; exit 0 }
function Test-SudoVdaPresent {
$devs = Get-PnpDevice -Class Display -PresentOnly -ErrorAction SilentlyContinue
foreach ($d in $devs) {
$hw = (Get-PnpDeviceProperty -InstanceId $d.InstanceId -KeyName 'DEVPKEY_Device_HardwareIds' `
-ErrorAction SilentlyContinue).Data
if ($hw -and ($hw | Where-Object { $_ -ieq $HardwareId })) { return $true }
}
return $false
}
$inf = Get-ChildItem -Path $Dir -Filter *.inf -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
$cer = Get-ChildItem -Path $Dir -Filter *.cer -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
$nef = Get-ChildItem -Path $Dir -Filter 'nefconc.exe' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $inf) { Write-Warning "no SudoVDA .inf in $Dir; skipping driver install."; exit 0 }
Write-Host "SudoVDA inf: $($inf.FullName)"
# 1) Trust the self-signed driver cert (a self-signed driver needs the cert in BOTH the machine
# Root store, so the chain validates, and TrustedPublisher, so PnP installs without a prompt).
if ($cer) {
Write-Host "==> importing $($cer.Name) to Root + TrustedPublisher"
certutil.exe -addstore -f Root "$($cer.FullName)" | Out-Null
certutil.exe -addstore -f TrustedPublisher "$($cer.FullName)" | Out-Null
}
else { Write-Warning "no .cer in $Dir — driver may not install silently (untrusted publisher)" }
# 2) Create the root device node only if it isn't already there.
if (Test-SudoVdaPresent) {
Write-Host "SudoVDA device node already present — leaving it as-is."
}
elseif ($nef) {
$infText = Get-Content -Raw $inf.FullName
$classGuid = ([regex]::Match($infText, '(?im)^\s*ClassGuid\s*=\s*(\{[0-9A-Fa-f-]+\})')).Groups[1].Value
$className = ([regex]::Match($infText, '(?im)^\s*Class\s*=\s*([^\s;]+)')).Groups[1].Value
if (-not $classGuid) { $classGuid = '{4d36e968-e325-11ce-bfc1-08002be10318}'; $className = 'Display' } # Display class
Write-Host "==> nefconc --create-device-node hwid=$HardwareId class=$className $classGuid"
& $nef.FullName --create-device-node --hardware-id $HardwareId --class-name $className --class-guid $classGuid
if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 3010) {
Write-Warning "nefconc --create-device-node returned $LASTEXITCODE"
}
}
else { Write-Warning "nefconc.exe not found in $Dir — cannot create the SudoVDA device node." }
# 3) Stage + bind the driver (idempotent; re-staging the same .inf is harmless).
Write-Host "==> pnputil /add-driver $($inf.Name) /install"
& pnputil.exe /add-driver "$($inf.FullName)" /install
$rc = $LASTEXITCODE
if ($rc -eq 3010) { Write-Host " driver installed; a reboot is recommended." }
elseif ($rc -ne 0) { Write-Warning "pnputil /add-driver returned $rc" }
exit 0
@@ -0,0 +1,57 @@
<#
.SYNOPSIS
Generate the NVENC import library (nvencodeapi.lib) into -OutDir, so the host links with
`--features nvenc` on a box that has no NVIDIA Video Codec SDK and no GPU.
.DESCRIPTION
The host links against nvencodeapi.lib (crates/punktfunk-host/build.rs). That import lib is just
a link-time stub for two exports of nvEncodeAPI64.dll (the real DLL ships with the NVIDIA driver
and resolves at runtime). We synthesise it from nvenc.def:
1. llvm-dlltool — preferred; LLVM is on the CI runner PATH (C:\Program Files\LLVM\bin) and this
works without a Visual Studio developer shell.
2. MSVC lib.exe — fallback; located via vswhere (no vcvars needed).
Point PUNKTFUNK_NVENC_LIB_DIR at -OutDir before `cargo build --features nvenc`.
.EXAMPLE
pwsh -File gen-nvenc-importlib.ps1 -OutDir C:\t\nvenc
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$OutDir,
[string]$DefPath = (Join-Path $PSScriptRoot 'nvenc.def')
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$PSNativeCommandUseErrorActionPreference = $false # check $LASTEXITCODE ourselves (pwsh 7.4 safe)
if (-not (Test-Path $DefPath)) { throw "module-definition file not found: $DefPath" }
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
$out = Join-Path $OutDir 'nvencodeapi.lib'
# 1) llvm-dlltool (preferred) ------------------------------------------------------------------
$dlltool = Get-Command llvm-dlltool -ErrorAction SilentlyContinue
if ($dlltool) {
Write-Host "==> llvm-dlltool -> $out"
& $dlltool.Source -m i386:x86-64 -d $DefPath -D nvEncodeAPI64.dll -l $out
if ($LASTEXITCODE -ne 0) { throw "llvm-dlltool failed ($LASTEXITCODE)" }
Write-Host " ok ($((Get-Item $out).Length) bytes)"
return
}
# 2) MSVC lib.exe via vswhere (fallback) -------------------------------------------------------
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
if (Test-Path $vswhere) {
$lib = & $vswhere -latest -prerelease -products * -find 'VC\Tools\MSVC\**\bin\Hostx64\x64\lib.exe' |
Select-Object -First 1
if ($lib -and (Test-Path $lib)) {
Write-Host "==> lib.exe -> $out"
& $lib "/def:$DefPath" /machine:x64 "/out:$out"
if ($LASTEXITCODE -ne 0) { throw "lib.exe failed ($LASTEXITCODE)" }
Write-Host " ok ($((Get-Item $out).Length) bytes)"
return
}
}
throw "neither llvm-dlltool (LLVM bin on PATH) nor MSVC lib.exe (via vswhere) was found to build $out"
+14
View File
@@ -0,0 +1,14 @@
; Module-definition file for the NVENC import library the host links against with `--features nvenc`.
;
; The real entry points live in nvEncodeAPI64.dll, which ships with the NVIDIA driver. At LINK time
; the host only needs an import library exporting these two symbols (see crates/punktfunk-host/build.rs:
; it emits `cargo:rustc-link-lib=dylib=nvencodeapi` and searches PUNKTFUNK_NVENC_LIB_DIR). No GPU,
; driver, or NVIDIA Video Codec SDK is required to BUILD only to run, where the DLL resolves from
; the installed driver. Generate nvencodeapi.lib from this file with gen-nvenc-importlib.ps1.
;
; The LIBRARY line names the DLL the import records point at required for MSVC `lib.exe /def`
; (without it the import name would default to "nvenc.dll"). llvm-dlltool takes the name from `-D`.
LIBRARY nvEncodeAPI64.dll
EXPORTS
NvEncodeAPICreateInstance
NvEncodeAPIGetMaxSupportedVersion
+143
View File
@@ -0,0 +1,143 @@
<#
.SYNOPSIS
Build + sign the punktfunk Windows host installer (Inno Setup setup.exe).
.DESCRIPTION
From a release `cargo build -p punktfunk-host --features nvenc` output (the exe), this:
1. resolves a code-signing cert (supplied stable .pfx from CI secrets OR an ephemeral self-signed
CN=unom — same scheme as the client's pack-msix.ps1) and exports the public .cer,
2. signs the inner punktfunk-host.exe,
3. fetches + stages the SudoVDA driver bundle (unless -NoDriver),
4. runs ISCC to build punktfunk-host-setup-<ver>.exe,
5. signs the setup.exe (timestamp best-effort),
6. emits HOST_SETUP_PATH / HOST_CER_PATH to GITHUB_ENV for the publish step.
Idempotent; safe to re-run. Run on the Windows runner / dev box (MSVC + Windows SDK + Inno Setup).
.EXAMPLE
pwsh -File pack-host-installer.ps1 -Version 0.2.137 -TargetDir C:\t\release -OutDir C:\t\out
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$Version, # e.g. 0.2.137 or 1.4.0 (free-form)
[Parameter(Mandatory = $true)][string]$TargetDir, # cargo --release dir (has punktfunk-host.exe)
[string]$OutDir = (Join-Path $TargetDir 'installer'),
[string]$Publisher = 'CN=unom',
[string]$PfxBase64 = $env:MSIX_CERT_PFX_B64, # reuse the client's signing secret
[string]$PfxPassword = $env:MSIX_CERT_PASSWORD,
[switch]$NoDriver, # build without the bundled SudoVDA driver
[switch]$NoSign # skip signing (local debug)
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
# Keep the traditional "check $LASTEXITCODE myself" model: don't let pwsh 7.4 turn a non-zero native
# exit into a terminating error (it would bypass Sign-File's timestamp-then-retry fallback below).
$PSNativeCommandUseErrorActionPreference = $false
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$iss = Join-Path $here 'punktfunk-host.iss'
$exe = Join-Path $TargetDir 'punktfunk-host.exe'
if (-not (Test-Path $exe)) { throw "missing build artifact 'punktfunk-host.exe' in $TargetDir (did 'cargo build --release -p punktfunk-host --features nvenc' run?)" }
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
# --- locate ISCC (Inno Setup) + signtool (Windows SDK) ---------------------------------------
function Find-Iscc {
foreach ($p in @(
'C:\Program Files (x86)\Inno Setup 6\ISCC.exe',
'C:\Program Files\Inno Setup 6\ISCC.exe')) {
if (Test-Path $p) { return $p }
}
$c = Get-Command iscc -ErrorAction SilentlyContinue
if ($c) { return $c.Source }
throw "ISCC.exe (Inno Setup 6, any 6.x) not found — install it (choco install innosetup -y)."
}
function Find-SdkTool([string]$name) {
$root = 'C:\Program Files (x86)\Windows Kits\10\bin'
$hit = Get-ChildItem -Path $root -Recurse -Filter $name -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match '\\(10\.0\.\d+\.\d+)\\x64\\' } |
Sort-Object { [version]([regex]::Match($_.FullName, '\\(10\.0\.\d+\.\d+)\\x64\\').Groups[1].Value) } |
Select-Object -Last 1
if (-not $hit) { throw "$name not found under $root — install the Windows 10/11 SDK." }
$hit.FullName
}
$iscc = Find-Iscc
Write-Host "ISCC: $iscc"
# --- signing cert (supplied stable pfx OR ephemeral self-signed) -----------------------------
$pfxPath = Join-Path $OutDir 'signing.pfx'
$cerPath = Join-Path $OutDir "punktfunk-host-windows_${Version}.cer"
$signtool = $null
if (-not $NoSign) {
$signtool = Find-SdkTool 'signtool.exe'
Write-Host "signtool: $signtool"
if ($PfxBase64) {
Write-Host "signing with supplied code-signing cert (MSIX_CERT_PFX_B64)"
[IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($PfxBase64))
}
else {
Write-Host "no MSIX_CERT_PFX_B64 -> generating an ephemeral self-signed cert (subject $Publisher)"
if (-not $PfxPassword) { $PfxPassword = 'punktfunk' }
$tmp = New-SelfSignedCertificate -Type Custom -Subject $Publisher `
-KeyUsage DigitalSignature -FriendlyName 'punktfunk host (self-signed)' `
-CertStoreLocation 'Cert:\CurrentUser\My' `
-TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3', '2.5.29.19={text}')
$sec = ConvertTo-SecureString -String $PfxPassword -Force -AsPlainText
Export-PfxCertificate -Cert "Cert:\CurrentUser\My\$($tmp.Thumbprint)" -FilePath $pfxPath -Password $sec | Out-Null
Remove-Item "Cert:\CurrentUser\My\$($tmp.Thumbprint)" -Force
}
# Always export the public .cer. For a self-signed cert it's the file users import once
# (LocalMachine\TrustedPublisher) so SmartScreen/UAC trusts the signed setup.exe; for a real CA
# cert it's a harmless extra.
$pwsec = if ($PfxPassword) { ConvertTo-SecureString -String $PfxPassword -Force -AsPlainText } else { $null }
$pubCert = if ($pwsec) { Get-PfxCertificate -FilePath $pfxPath -Password $pwsec } else { Get-PfxCertificate -FilePath $pfxPath }
Export-Certificate -Cert $pubCert -FilePath $cerPath | Out-Null
Write-Host "signing cert subject=$($pubCert.Subject) thumbprint=$($pubCert.Thumbprint)"
}
function Sign-File([string]$Path) {
if ($NoSign) { return }
$signArgs = @('sign', '/fd', 'SHA256', '/f', $pfxPath)
if ($PfxPassword) { $signArgs += @('/p', $PfxPassword) }
& $signtool ($signArgs + @('/tr', 'http://timestamp.digicert.com', '/td', 'SHA256', $Path))
if ($LASTEXITCODE -ne 0) {
Write-Warning "timestamped sign failed for $Path — retrying without a timestamp"
& $signtool ($signArgs + @($Path))
if ($LASTEXITCODE -ne 0) { throw "signtool sign failed for $Path ($LASTEXITCODE)" }
}
}
# --- sign the inner exe before it's packed ----------------------------------------------------
Sign-File $exe
# --- stage the SudoVDA driver bundle ----------------------------------------------------------
$defines = @("/DMyAppVersion=$Version", "/DBinDir=$TargetDir", "/DOutputDir=$OutDir")
if (-not $NoDriver) {
$stage = Join-Path $OutDir 'stage'
& (Join-Path $here 'fetch-sudovda.ps1') -OutDir $stage
Copy-Item (Join-Path $here 'install-sudovda.ps1') (Join-Path $stage 'install-sudovda.ps1') -Force
$defines += "/DStageDir=$stage"
}
else { Write-Host "-NoDriver: building installer WITHOUT the bundled SudoVDA driver" }
# --- build the installer ----------------------------------------------------------------------
Write-Host "==> ISCC $($defines -join ' ') $iss"
& $iscc @defines $iss
if ($LASTEXITCODE -ne 0) { throw "ISCC failed ($LASTEXITCODE)" }
$setup = Join-Path $OutDir "punktfunk-host-setup-$Version.exe"
if (-not (Test-Path $setup)) { throw "expected installer not produced: $setup" }
# --- sign the setup.exe + clean up ------------------------------------------------------------
Sign-File $setup
Remove-Item $pfxPath -Force -ErrorAction SilentlyContinue
Write-Host ""
Write-Host "==> installer: $setup"
if (-not $NoSign) {
Write-Host "==> trust the cert once per machine (self-signed builds), then the signed setup.exe is trusted:"
Write-Host " Import-Certificate -FilePath '$cerPath' -CertStoreLocation Cert:\LocalMachine\TrustedPublisher"
}
if ($env:GITHUB_ENV) {
"HOST_SETUP_PATH=$setup" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
if (-not $NoSign) { "HOST_CER_PATH=$cerPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 }
}
+104
View File
@@ -0,0 +1,104 @@
; punktfunk host installer (Inno Setup 6).
;
; Produces a signed setup.exe that lays the host into Program Files, optionally installs the bundled
; SudoVDA virtual-display driver, and DELEGATES service registration to `punktfunk-host service
; install`. The real, idempotent install logic (SCM registration, firewall rules, default host.env,
; the SYSTEM→interactive-session CreateProcessAsUserW supervisor for secure-desktop capture) lives in
; crates/punktfunk-host/src/service.rs — this script does NOT duplicate it. That SYSTEM service model
; is exactly why MSIX is unusable here and we ship a classic elevated installer instead.
;
; Built by pack-host-installer.ps1, e.g.:
; ISCC.exe /DMyAppVersion=0.2.123 /DBinDir=C:\t\release /DStageDir=C:\t\out\stage \
; /DOutputDir=C:\t\out packaging\windows\punktfunk-host.iss
; Omit /DStageDir to build an installer WITHOUT the bundled driver (driver becomes a prerequisite).
#ifndef MyAppVersion
#define MyAppVersion "0.0.0"
#endif
#ifndef BinDir
#define BinDir "."
#endif
#ifndef OutputDir
#define OutputDir "."
#endif
; StageDir (the staged SudoVDA payload + nefconc.exe + install-sudovda.ps1) is optional.
#ifdef StageDir
#define WithDriver
#endif
[Setup]
AppId={{7C9E6A52-1F4B-4E8D-A3C7-2B5D8F1E0A93}
AppName=punktfunk host
AppVersion={#MyAppVersion}
AppPublisher=unom
AppPublisherURL=https://git.unom.io/unom/punktfunk
DefaultDirName={autopf}\punktfunk
DefaultGroupName=punktfunk
DisableProgramGroupPage=yes
UsePreviousAppDir=yes
PrivilegesRequired=admin
MinVersion=10.0
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64
OutputDir={#OutputDir}
OutputBaseFilename=punktfunk-host-setup-{#MyAppVersion}
Compression=lzma2/max
SolidCompression=yes
WizardStyle=modern
UninstallDisplayName=punktfunk host {#MyAppVersion}
UninstallDisplayIcon={app}\punktfunk-host.exe
[Tasks]
#ifdef WithDriver
Name: "installdriver"; Description: "Install the SudoVDA virtual display driver (required for native-resolution streaming)"
#endif
Name: "startservice"; Description: "Start the punktfunk host service now (also starts on every boot)"
[Files]
Source: "{#BinDir}\punktfunk-host.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}..\..\scripts\windows\host.env.example"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}README.md"; DestDir: "{app}"; DestName: "README.txt"; Flags: ignoreversion
#ifdef WithDriver
; The driver payload + nefconc.exe + install-sudovda.ps1, extracted to {tmp} and removed after install.
Source: "{#StageDir}\*"; DestDir: "{tmp}\sudovda"; Flags: deleteafterinstall recursesubdirs createallsubdirs; Tasks: installdriver
#endif
[Run]
#ifdef WithDriver
Filename: "powershell.exe"; \
Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{tmp}\sudovda\install-sudovda.ps1"" -Dir ""{tmp}\sudovda"""; \
StatusMsg: "Installing the SudoVDA virtual display driver..."; \
Flags: runhidden waituntilterminated; Tasks: installdriver
#endif
; Register (or re-point, on upgrade — idempotent) the SYSTEM service from its FINAL {app} location:
; service install records current_exe() as the SCM binPath, so it must run from {app}, not {tmp}.
Filename: "{app}\punktfunk-host.exe"; Parameters: "service install"; WorkingDir: "{app}"; \
StatusMsg: "Registering the punktfunk host service..."; Flags: runhidden waituntilterminated
Filename: "{app}\punktfunk-host.exe"; Parameters: "service start"; WorkingDir: "{app}"; \
StatusMsg: "Starting the punktfunk host service..."; Flags: runhidden waituntilterminated; Tasks: startservice
[UninstallRun]
Filename: "{app}\punktfunk-host.exe"; Parameters: "service uninstall"; Flags: runhidden waituntilterminated; RunOnceId: "PunktfunkHostServiceUninstall"
[Code]
{ On upgrade the running service locks punktfunk-host.exe (and the supervisor would respawn it from
the OLD binary), so stop it and WAIT for STOPPED before files are copied. Best-effort; a fresh
install is a no-op (the service doesn't exist yet). }
procedure StopHostServiceAndWait;
var
ResultCode: Integer;
begin
Exec('powershell.exe',
'-NoProfile -ExecutionPolicy Bypass -Command "' +
'$ErrorActionPreference=''SilentlyContinue''; ' +
'$s=Get-Service -Name ''PunktfunkHost''; ' +
'if($s -and $s.Status -ne ''Stopped''){Stop-Service -Name ''PunktfunkHost'' -Force; ' +
'try{$s.WaitForStatus(''Stopped'',[TimeSpan]::FromSeconds(30))}catch{}}"',
'', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssInstall then
StopHostServiceAndWait;
end;