feat(windows-installer): move driver + web install into the host exe (ASCII root fix)
apple / swift (push) Successful in 1m0s
apple / screenshots (push) Successful in 5m16s
windows-host / package (push) Successful in 6m25s
ci / rust (push) Failing after 28s
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 1m1s
android / android (push) Successful in 3m21s
deb / build-publish (push) Successful in 2m31s
decky / build-publish (push) Successful in 11s
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 5s
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 6s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m39s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m2s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m50s
docker / deploy-docs (push) Successful in 17s

Port the three install-time PowerShell *files* (install-pf-vdisplay.ps1,
install-gamepad-drivers.ps1, web-setup.ps1) into punktfunk-host.exe subcommands:
`driver install [--gamepad] --dir <stage>` and `web setup --app-dir <app>
[--password-file <f>]` (windows/install.rs).

Why: PowerShell 5.1 reads a BOM-less .ps1 FILE in the machine ANSI codepage, so a
stray non-ASCII byte mis-decodes and aborts on a non-English box - exactly how the
pf-vdisplay driver install silently failed. A compiled subcommand drives the same
external tools (certutil/pnputil/nefconc/schtasks/netsh/icacls) as fixed string
literals, with no file-codepage surface. (The .iss's INLINE -Command PowerShell is a
command-line string, not a file read, so it's unaffected and stays.)

- windows/install.rs: faithful port - cert trust, gated nefconc node create + pnputil
  for pf-vdisplay; pnputil per-inf for gamepads; web-password ACL, the PunktfunkWeb task
  (generated UTF-16 XML), firewall rule, start. Best-effort (a hiccup warns, never aborts).
- punktfunk-host.iss [Run]: call the exe instead of `powershell -File`; drop the
  web-setup.ps1 staging + WebSetup define; WebSetupParams emits --app-dir/--password-file.
- pack-host-installer.ps1: stop copying the three install scripts into the stages.
- delete the three .ps1 files.

The `mod install;` + dispatch arms in main.rs landed in the preceding docs commit
(swept up by a concurrent commit); this commit adds the module + installer wiring.
CI-compile-validated via windows-host; the install path is on-glass-validated on the
next canary install (the test box is offline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 16:43:18 +00:00
parent 7b99b41ede
commit 125a51d81d
6 changed files with 411 additions and 263 deletions
@@ -1,50 +0,0 @@
<#
.SYNOPSIS
Install the bundled punktfunk virtual-gamepad UMDF drivers - pf_dualsense (DualSense + DualShock 4,
one type-aware HID driver) and pf_xusb (Xbox 360 XUSB companion for classic XInput). Runs ELEVATED
at setup time (invoked from the installer's [Run] section). Best-effort: warns and exits 0 on any
failure, so a driver hiccup never aborts the whole install (gamepad input degrades gracefully - a
session still streams without a pad).
.DESCRIPTION
-Dir holds the staged payload: pf_dualsense.{inf,cat,dll}, pf_xusb.{inf,cat,dll}, and the signing
.cer. Steps:
1. Trust the self-signed driver cert (machine Root + TrustedPublisher) so pnputil adds it silently.
2. pnputil /add-driver each .inf - adds the package to the driver store. (No /install or device-node
creation: the host SwDeviceCreate's the per-session devnodes itself when a client forwards a pad,
so PnP binds the store driver on demand.)
ASCII-only on purpose: this is run by the installer via Windows PowerShell 5.1, which mis-decodes a
BOM-less UTF-8 non-ASCII char (e.g. an em-dash) as a smart-quote and breaks parsing.
.EXAMPLE
powershell -ExecutionPolicy Bypass -File install-gamepad-drivers.ps1 -Dir C:\path\to\gamepad
#>
[CmdletBinding()]
param([string]$Dir = $PSScriptRoot)
# Never abort the installer on a driver failure.
$ErrorActionPreference = 'Continue'
trap { Write-Warning "gamepad driver install error: $_"; exit 0 }
# 1) Trust the self-signed driver cert (Root so the chain validates + TrustedPublisher so pnputil adds
# it without a prompt).
$cer = Get-ChildItem -Path $Dir -Filter *.cer -ErrorAction SilentlyContinue | Select-Object -First 1
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; drivers may not install silently (untrusted publisher)" }
# 2) Add each driver package to the store (idempotent; re-adding the same .inf is harmless).
$infs = Get-ChildItem -Path $Dir -Filter *.inf -ErrorAction SilentlyContinue
if (-not $infs) { Write-Warning "no driver .inf in $Dir; skipping gamepad driver install."; exit 0 }
foreach ($inf in $infs) {
Write-Host "==> pnputil /add-driver $($inf.Name)"
& pnputil.exe /add-driver "$($inf.FullName)"
$rc = $LASTEXITCODE
if ($rc -eq 3010) { Write-Host " added; a reboot is recommended." }
elseif ($rc -ne 0) { Write-Warning "pnputil /add-driver $($inf.Name) returned $rc" }
}
exit 0
-82
View File
@@ -1,82 +0,0 @@
<#
.SYNOPSIS
Install the bundled pf-vdisplay (punktfunk) virtual-display driver - our own all-Rust UMDF IddCx
indirect-display driver, built from source per release (packaging/windows/build-pf-vdisplay.ps1).
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 a virtual display, so a driver
hiccup must never abort the whole install.
.DESCRIPTION
-Dir holds the staged payload (pf_vdisplay.inf/.cat/.dll + signing .cer + nefconc.exe). Steps:
1. Trust the self-signed driver cert (machine Root + TrustedPublisher) so PnP installs it silently
(the punktfunk-driver cert the build signs the driver + catalog with).
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/windows/pf_vdisplay.rs).
ALWAYS via nefconc (a clean ROOT\DISPLAY node) - NEVER devgen, which makes persistent SWD\DEVGEN
software devices that survive reboot + registry deletion and resurrect on every driver install.
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-pf-vdisplay.ps1 -Dir C:\path\to\pf-vdisplay
#>
[CmdletBinding()]
param(
[string]$Dir = $PSScriptRoot,
[string]$HardwareId = 'root\pf_vdisplay' # matches pf_vdisplay.inf [Standard.NTamd64]
)
# Never abort the installer on a driver failure.
$ErrorActionPreference = 'Continue'
trap { Write-Warning "pf-vdisplay install error: $_"; exit 0 }
function Test-PfVdisplayPresent {
$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 pf_vdisplay.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 pf_vdisplay.inf in $Dir; skipping driver install."; exit 0 }
Write-Host "pf-vdisplay 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. nefconc, NEVER devgen.
if (Test-PfVdisplayPresent) {
Write-Host "pf-vdisplay 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 pf-vdisplay 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
+5 -7
View File
@@ -152,7 +152,7 @@ if (-not $NoDriver) {
& (Join-Path $here 'build-pf-vdisplay.ps1') -Out $built
$stage = Join-Path $OutDir 'stage'
& (Join-Path $here 'stage-pf-vdisplay.ps1') -OutDir $stage -VendorDir $built
Copy-Item (Join-Path $here 'install-pf-vdisplay.ps1') (Join-Path $stage 'install-pf-vdisplay.ps1') -Force
# The installer runs `punktfunk-host.exe driver install --dir {tmp}\pfvdisplay` (not a staged .ps1).
$defines += "/DStageDir=$stage"
}
else { Write-Host "-NoDriver: building installer WITHOUT the bundled pf-vdisplay driver" }
@@ -160,8 +160,9 @@ else { Write-Host "-NoDriver: building installer WITHOUT the bundled pf-vdisplay
# --- build (from source) + stage the punktfunk virtual-gamepad UMDF drivers --------------------
# pf-dualsense (DualSense / DualShock 4) + pf-xusb (Xbox 360 / XInput) are members of the same drivers
# workspace as pf-vdisplay, built from source per release (build-gamepad-drivers.ps1) - same anti-stale
# reasoning as pf-vdisplay; the prior checked-in binaries under gamepad-drivers/ are retired. install-
# gamepad-drivers.ps1 adds each to the store (the host SwDeviceCreate's the per-session devnodes).
# reasoning as pf-vdisplay; the prior checked-in binaries under gamepad-drivers/ are retired. The
# installer adds each to the store via `punktfunk-host.exe driver install --gamepad` (the host
# SwDeviceCreate's the per-session devnodes).
if (-not $NoDriver) {
$gpBuilt = Join-Path $OutDir 'gamepad-built'
# -SkipBuild: build-pf-vdisplay.ps1 above already `cargo build`s the WHOLE drivers workspace (incl.
@@ -171,7 +172,6 @@ if (-not $NoDriver) {
if (Test-Path $gpStage) { Remove-Item -Recurse -Force $gpStage }
New-Item -ItemType Directory -Force -Path $gpStage | Out-Null
Copy-Item (Join-Path $gpBuilt '*') $gpStage -Force
Copy-Item (Join-Path $here 'install-gamepad-drivers.ps1') (Join-Path $gpStage 'install-gamepad-drivers.ps1') -Force
$defines += "/DGamepadStageDir=$gpStage"
Write-Host "==> built + staged gamepad UMDF drivers -> $gpStage"
}
@@ -208,13 +208,11 @@ if ($WebDir -and (Test-Path $WebDir) -and $BunExe -and (Test-Path $BunExe)) {
$bunStage = Join-Path $OutDir 'bun.exe'
Copy-Item -LiteralPath $BunExe -Destination $bunStage -Force
$webRun = Join-Path $OutDir 'web-run.cmd'
$webSetup = Join-Path $OutDir 'web-setup.ps1'
Copy-Item (Join-Path $repoRoot 'scripts\windows\web-run.cmd') -Destination $webRun -Force
Copy-Item (Join-Path $repoRoot 'scripts\windows\web-setup.ps1') -Destination $webSetup -Force
# The console is provisioned by `punktfunk-host.exe web setup` (not a staged web-setup.ps1).
$defines += "/DWebDir=$webStage"
$defines += "/DBunExe=$bunStage"
$defines += "/DWebRunCmd=$webRun"
$defines += "/DWebSetup=$webSetup"
Write-Host "bundling the web console from $WebDir (+ bun $BunExe)"
}
else { Write-Host "no -WebDir/-BunExe -> installer built WITHOUT the web console" }
+7 -14
View File
@@ -33,9 +33,6 @@
#ifndef WebRunCmd
#define WebRunCmd "..\..\scripts\windows\web-run.cmd"
#endif
#ifndef WebSetup
#define WebSetup "..\..\scripts\windows\web-setup.ps1"
#endif
; StageDir (the staged pf-vdisplay payload + nefconc.exe + install-pf-vdisplay.ps1) is optional.
#ifdef StageDir
#define WithDriver
@@ -114,12 +111,11 @@ Source: "{#FfmpegBin}\*.dll"; DestDir: "{app}"; Flags: ignoreversion
#ifdef WithWeb
; The web management console: the self-contained Nitro SSR bundle (.output = server + public; deps
; bundled in, no node_modules) -> {app}\web\.output, a portable bun runtime -> {app}\bun\bun.exe, and
; the launcher the PunktfunkWeb task runs -> {app}\web\web-run.cmd. web-setup.ps1 (the provisioner)
; goes to {tmp} and is removed after install.
; the launcher the PunktfunkWeb task runs -> {app}\web\web-run.cmd. (`punktfunk-host.exe web setup`
; provisions the console at install time - no staged provisioner script.)
Source: "{#WebDir}\*"; DestDir: "{app}\web\.output"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#BunExe}"; DestDir: "{app}\bun"; DestName: "bun.exe"; Flags: ignoreversion
Source: "{#WebRunCmd}"; DestDir: "{app}\web"; DestName: "web-run.cmd"; Flags: ignoreversion
Source: "{#WebSetup}"; DestDir: "{tmp}"; DestName: "web-setup.ps1"; Flags: deleteafterinstall
#endif
#ifdef WithDriver
; The driver payload + nefconc.exe + install-pf-vdisplay.ps1, extracted to {tmp} and removed after install.
@@ -148,14 +144,12 @@ Root: HKLM64; Subkey: "SOFTWARE\Khronos\Vulkan\ImplicitLayers"; ValueType: dword
[Run]
#ifdef WithDriver
Filename: "powershell.exe"; \
Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{tmp}\pfvdisplay\install-pf-vdisplay.ps1"" -Dir ""{tmp}\pfvdisplay"""; \
Filename: "{app}\punktfunk-host.exe"; Parameters: "driver install --dir ""{tmp}\pfvdisplay"""; WorkingDir: "{app}"; \
StatusMsg: "Installing the pf-vdisplay virtual display driver..."; \
Flags: runhidden waituntilterminated; Tasks: installdriver
#endif
#ifdef WithGamepad
Filename: "powershell.exe"; \
Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{tmp}\gamepad\install-gamepad-drivers.ps1"" -Dir ""{tmp}\gamepad"""; \
Filename: "{app}\punktfunk-host.exe"; Parameters: "driver install --gamepad --dir ""{tmp}\gamepad"""; WorkingDir: "{app}"; \
StatusMsg: "Installing the virtual gamepad drivers..."; \
Flags: runhidden waituntilterminated; Tasks: installgamepad
#endif
@@ -169,8 +163,7 @@ Filename: "{app}\punktfunk-host.exe"; Parameters: "service start"; WorkingDir: "
; Provision the console AFTER the host service is up (so the mgmt token exists): write the ACL'd
; login password, register the PunktfunkWeb scheduled task (boot, SYSTEM, restart-on-failure),
; open TCP 3000, and start it. {code:WebSetupParams} appends -PasswordFile only on a fresh install.
Filename: "powershell.exe"; \
Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{tmp}\web-setup.ps1"" {code:WebSetupParams}"; \
Filename: "{app}\punktfunk-host.exe"; Parameters: "web setup {code:WebSetupParams}"; WorkingDir: "{app}"; \
StatusMsg: "Setting up the punktfunk web console..."; Flags: runhidden waituntilterminated
#endif
@@ -265,9 +258,9 @@ function WebSetupParams(Param: String): String;
begin
{ Pass the password to web-setup.ps1 via a temp file, not the cmdline (which lands in the install
log). Only on a fresh install - on upgrade web-setup keeps the existing file. }
Result := '-AppDir "' + ExpandConstant('{app}') + '"';
Result := '--app-dir "' + ExpandConstant('{app}') + '"';
if FreshWebInstall then
Result := Result + ' -PasswordFile "' + ExpandConstant('{tmp}\webpw.txt') + '"';
Result := Result + ' --password-file "' + ExpandConstant('{tmp}\webpw.txt') + '"';
end;
#endif