ci / rust (push) Failing after 12s
windows-drivers / probe-and-proto (push) Successful in 48s
ci / web (push) Successful in 1m1s
ci / docs-site (push) Successful in 1m6s
deb / build-publish-client-arm64 (push) Failing after 10s
decky / build-publish (push) Successful in 47s
windows-drivers / driver-build (push) Successful in 1m40s
apple / swift (push) Successful in 3m6s
ci / bench (push) Successful in 7m39s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 1m0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8m2s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m0s
android / android (push) Successful in 12m28s
deb / build-publish (push) Successful in 12m13s
ci / rust-arm64 (push) Successful in 12m31s
arch / build-publish (push) Successful in 12m40s
deb / build-publish-host (push) Successful in 12m17s
windows-host / package (push) Successful in 18m26s
windows-host / winget-source (push) Skipped
apple / screenshots (push) Successful in 23m25s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m34s
docker / build-push-arm64cross (push) Successful in 8s
docker / deploy-docs (push) Successful in 31s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m24s
A LocalService principal could take over a virtual pad's shared input section and
forge HID input into the interactive desktop.
The host duplicates each pad's unnamed DATA section into the driver's WUDFHost, and
through gamepad proto v2 it learned that process from `driver_pid` in the named
bootstrap mailbox. That mailbox has to be LocalService-writable — that is what the
driver's own WUDFHost runs as — and the delivery gate, verify_is_wudfhost, only checks
that the target's IMAGE is %SystemRoot%\System32\WUDFHost.exe. That image is
world-executable. So anything running as LocalService — notably the deliberately
de-privileged plugin runner — could spawn its own WUDFHost (CREATE_SUSPENDED parks it
indefinitely with the right image path), publish that pid, and be handed
SECTION_MAP_READ|WRITE on a live section. For pf-mouse that section drives a real
absolute pointer, so it was desktop control; for the pads it was forged gamepad input
plus a read of the remote user's controller state.
The module docs claimed mailbox tampering "yields at worst a gamepad DoS, never a read
or an injection". That was wrong, and the reasoning behind it — that a LocalService
token is DACL-denied OpenProcess on a UMDF WUDFHost — only covers the REAL host, not
one the attacker spawned itself.
The pid now comes from the device stack (ChannelProof, proto 2 -> 3). The host asks the
devnode it SwDeviceCreate'd who is serving it, looked up by the instance id PnP handed
back, so a planted look-alike devnode is not a candidate and the kernel — not anything
the attacker supplies — does the routing. Only the driver PnP actually bound to that
device can answer. `driver_pid` survives as a liveness hint; a tamperer can still deny a
pad, which squatting the name always allowed, but can no longer choose the recipient.
Two rules keep the state machine honest around it: a delivery stands until its target
process EXITS (judged on a retained SYNCHRONIZE handle, so a recycled pid cannot fake
it, and UMDF's restart-after-driver-crash still re-attaches), and a pad with no
SwDeviceCreate devnode refuses to deliver rather than fall back — unless an operator
sets PUNKTFUNK_PAD_CHANNEL_TRUST_MAILBOX, which says so loudly.
Three transports, because Windows carries different things to different driver shapes,
and the obvious two did not survive contact with hidclass. Measured on .173 (Win11
26200): HidD_GetIndexedString is NOT forwarded to a UMDF HID minidriver at all — it
failed for every index including ones the driver demonstrably serves through the named
wrappers; and a private device interface registers and enumerates but cannot be OPENED
(ERROR_GEN_FAILURE), because hidclass owns IRP_MJ_CREATE on a devnode it is the FDO for.
That is exactly why pf-xusb was never affected: it is not a HID minidriver, so nothing
sits above it. What works:
* pf-xusb — a private IOCTL on its own GUID_DEVINTERFACE_XUSB.
* pf-mouse — the HID serial string. Verified: PFCP:3:0:7296, and 7296 was a genuine
service-spawned WUDFHost.exe. Safe here alone: nothing reads the virtual
mouse's serial, whereas a pad's is SDL/Steam dedup material.
* pf-gamepad — a HID feature report, and it cost NO report-descriptor change. The
captured descriptors already declare far more Feature ids than the driver
ever served: 0x85 is declared on DualSense, DualShock 4 and Edge alike and
used to fail with STATUS_INVALID_PARAMETER, so hidclass lets it through and
nothing can have depended on the old failure. The Deck's one feature report
is unnumbered and Steam drives it command->response, so its proof rides that
existing contract via a private two-byte command. Verified: feature 0x85
returned magic "PFCP", proto 3, pad_index 0, wudf_pid 18456 — and 18456 was
a WUDFHost — with the product string still 'DualSense Wireless Controller'.
Also renamed pf-dualsense -> pf-gamepad. One driver has always served four identities, so
the old name read as if the other three lived elsewhere. ONLY the package identity moved
(crate, INF/CAT/DLL, UMDF service, build script, CI lines, log file, env var). The four
HARDWARE IDS are deliberately unchanged — they bind every devnode the host creates and
every installed system — as are the Global\pfds-boot-<i> mailbox and PAD_MAGIC, which are
wire contract. `driver install --gamepad` now retires the pre-rename store package first,
matched on pf_dualsense.dll because that string appears only in the OLD inf; matching on
the hardware ids would delete what we are about to install. On .173 that separated 14
stale packages from the 1 new one with 0 ambiguous, and the renamed package binds the old
hwid (devgen root\pf_dualsense -> oem143.inf = pf_gamepad.inf).
The repo's own pre-commit/pre-push rustfmt hooks named the old crate, so they caught the
rename before the commit did — they now check pf-gamepad, and pf-mouse alongside it, which
they had been missing relative to the CI line.
Host and drivers MUST ship together: v2<->v3 fails closed in both directions by design,
with the existing "update host + drivers together" diagnostic.
The rename moved files that also carry the security change, so splitting this into two
commits would mean reconstructing an intermediate state that was never gated. It is one
commit on purpose.
Gated on the windows-amd64 runner with cargo clean first (the box's clock lags, so stale
artifacts would read as a vacuous green): clippy -D warnings clean for pf-inject,
pf-capture and pf-driver-proto, drivers workspace build + the CI clippy line clean,
cargo check --release -p punktfunk-host clean, 19 + 58 tests green. Also fixes pf-mouse
still writing its debug log to world-writable C:\Users\Public, which the 2026-07-17
review moved for the other three drivers and missed here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
360 lines
22 KiB
PowerShell
360 lines
22 KiB
PowerShell
<#
|
|
.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. stages the pf-vdisplay virtual-display 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,
|
|
[string]$FfmpegDir = $env:FFMPEG_DIR, # bundle its bin\*.dll (amf-qsv build)
|
|
[string]$WebDir = $env:WEB_OUTPUT_DIR, # built web .output tree -> bundle the mgmt console
|
|
[string]$ScriptingBundle = $env:SCRIPTING_BUNDLE, # built runner-cli.js -> bundle the plugin/script runner
|
|
[string]$BunExe = $env:BUN_EXE, # portable bun.exe runtime for the console + runner
|
|
[string]$VbCableDir = $env:VBCABLE_DIR, # official base VB-CABLE package -> bundle the virtual mic
|
|
[switch]$NoDriver, # build without the bundled pf-vdisplay 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?)" }
|
|
$trayExe = Join-Path $TargetDir 'punktfunk-tray.exe'
|
|
if (-not (Test-Path $trayExe)) { throw "missing build artifact 'punktfunk-tray.exe' in $TargetDir (did 'cargo build --release -p punktfunk-tray' 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 exes before they're packed -------------------------------------------------
|
|
Sign-File $exe
|
|
Sign-File $trayExe
|
|
|
|
# --- resolve + validate the installer's source files ------------------------------------------
|
|
$repoRoot = (Resolve-Path (Join-Path $here '..\..')).Path
|
|
$hostEnvSrc = Join-Path $repoRoot 'scripts\windows\host.env.example'
|
|
$readmeSrc = Join-Path $here 'README.md'
|
|
foreach ($p in @($exe, $trayExe, $hostEnvSrc, $readmeSrc, $iss)) {
|
|
if (-not (Test-Path -LiteralPath $p)) { throw "installer source file missing: $p" }
|
|
}
|
|
|
|
# ISCC is a 32-bit program. On the self-hosted runner (which runs as SYSTEM) the checkout lives
|
|
# under C:\Windows\System32\config\systemprofile\..., and WOW64 file-system redirection rewrites a
|
|
# 32-bit process's System32 reads to SysWOW64 (where the files don't exist) -> ISCC dies at
|
|
# script-open with "path not found". So stage every file ISCC reads (the .iss + the two payload
|
|
# files) into the non-redirected build dir under C:\t. (BinDir/StageDir/OutputDir already live there.)
|
|
$hostEnv = Join-Path $OutDir 'host.env.example'
|
|
$readme = Join-Path $OutDir 'README.md'
|
|
$issLocal = Join-Path $OutDir 'punktfunk-host.iss'
|
|
Copy-Item -LiteralPath $hostEnvSrc -Destination $hostEnv -Force
|
|
Copy-Item -LiteralPath $readmeSrc -Destination $readme -Force
|
|
Copy-Item -LiteralPath $iss -Destination $issLocal -Force
|
|
# Branding (wizard BMPs + punktfunk.ico, committed outputs of branding/gen-branding.ps1): the .iss
|
|
# references them as "branding\" relative to itself, so stage the dir next to the staged .iss.
|
|
$brandStage = Join-Path $OutDir 'branding'
|
|
if (Test-Path $brandStage) { Remove-Item $brandStage -Recurse -Force }
|
|
New-Item -ItemType Directory -Force -Path $brandStage | Out-Null
|
|
Copy-Item (Join-Path $here 'branding\*.bmp') $brandStage -Force
|
|
Copy-Item (Join-Path $here 'branding\punktfunk.ico') $brandStage -Force
|
|
|
|
# License/attribution payload bundled into {app}\licenses: the project's own MIT/Apache texts and the
|
|
# generated third-party crate notices. The FFmpeg LGPL notice + license text are added to this same
|
|
# dir below when the AMF/QSV FFmpeg DLLs are bundled. (THIRD-PARTY-NOTICES.txt is committed; CI may
|
|
# regenerate it via scripts/gen-third-party-notices.sh before packaging.)
|
|
$licStage = Join-Path $OutDir 'licenses'
|
|
New-Item -ItemType Directory -Force -Path $licStage | Out-Null
|
|
foreach ($n in @('LICENSE-MIT', 'LICENSE-APACHE', 'THIRD-PARTY-NOTICES.txt')) {
|
|
$p = Join-Path $repoRoot $n
|
|
if (Test-Path $p) { Copy-Item $p -Destination $licStage -Force }
|
|
else { Write-Warning "license payload missing (skipped): $p" }
|
|
}
|
|
|
|
$defines = @(
|
|
"/DMyAppVersion=$Version",
|
|
"/DBinDir=$TargetDir",
|
|
"/DOutputDir=$OutDir",
|
|
"/DHostEnv=$hostEnv",
|
|
"/DReadme=$readme",
|
|
"/DLicensesDir=$licStage"
|
|
)
|
|
|
|
# --- build (from source) + stage the pf-vdisplay virtual-display driver -----------------------
|
|
# pf-vdisplay is our all-Rust IddCx driver (packaging/windows/drivers/). It is now BUILT FROM SOURCE
|
|
# every release (build-pf-vdisplay.ps1) instead of shipping a checked-in prebuilt binary: the vendored
|
|
# binary went stale (its .cat stopped covering an edited .inf -> pnputil SPAPI_E_FILE_HASH_NOT_IN_CATALOG
|
|
# on every box, and it predated IOCTL_SET_RENDER_ADAPTER the host needs on hybrid/Optimus GPUs). Building
|
|
# here keeps the .dll/.inf/.cat in lockstep + ships current driver features. stage-pf-vdisplay.ps1 then
|
|
# adds the fetched nefcon device tool. (Needs the WDK build env; -NoDriver skips it for a WDK-less pack.)
|
|
if (-not $NoDriver) {
|
|
$built = Join-Path $OutDir 'pfvd-built'
|
|
& (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
|
|
# 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" }
|
|
|
|
# --- build (from source) + stage the punktfunk virtual-gamepad UMDF drivers --------------------
|
|
# pf-gamepad (DualSense / DS4 / Edge / Deck) + 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. 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.
|
|
# the gamepad cdylibs), so just sign+stage them here - no redundant second full build.
|
|
& (Join-Path $here 'build-gamepad-drivers.ps1') -Out $gpBuilt -SkipBuild
|
|
$gpStage = Join-Path $OutDir 'gamepad'
|
|
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
|
|
$defines += "/DGamepadStageDir=$gpStage"
|
|
Write-Host "==> built + staged gamepad UMDF drivers -> $gpStage"
|
|
}
|
|
|
|
# --- stage the official base VB-CABLE package (the streaming virtual microphone) --------------
|
|
# VB-CABLE is the virtual audio cable the host writes the client's mic into (its capture endpoint then
|
|
# surfaces as a host microphone). We bundle + silently install the OFFICIAL base VB-CABLE package
|
|
# (VB-Audio donationware, redistributed under VB-Audio's bundling grant - see the VB-CABLE notice added
|
|
# to the licenses payload). The package binary is NOT in the repo (it's a signed third-party blob,
|
|
# shipped intact); supply it via -VbCableDir / $env:VBCABLE_DIR pointing at the extracted official
|
|
# package (must contain VBCABLE_Setup_x64.exe). Absent -> installer built WITHOUT the bundled cable; the
|
|
# host then auto-installs the Steam Streaming pair as a fallback and mic passthrough needs a manual cable.
|
|
if ($VbCableDir -and -not ((Test-Path $VbCableDir) -and (Get-ChildItem -Path $VbCableDir -Filter 'VBCABLE_Setup*.exe' -ErrorAction SilentlyContinue))) {
|
|
# An explicitly-supplied dir that doesn't hold the package is a broken provisioning, not an
|
|
# opt-out - fail loudly instead of silently shipping an installer without the virtual mic
|
|
# (exactly the field regression this bundling fixes). Opt out by leaving VBCABLE_DIR unset.
|
|
throw "VbCableDir '$VbCableDir' has no VBCABLE_Setup*.exe - re-run scripts/ci/provision-windows-punktfunk-extras.ps1 (or unset VBCABLE_DIR to build without the virtual mic)"
|
|
}
|
|
if ($VbCableDir) {
|
|
$vbStage = Join-Path $OutDir 'vbcable'
|
|
if (Test-Path $vbStage) { Remove-Item -Recurse -Force $vbStage }
|
|
New-Item -ItemType Directory -Force -Path $vbStage | Out-Null
|
|
Copy-Item (Join-Path $VbCableDir '*') $vbStage -Recurse -Force
|
|
# The on-target installer script (seeds VB-Audio's cert into TrustedPublisher, runs -i -h) ships
|
|
# alongside the package so it's extracted to the same {tmp}\vbcable dir.
|
|
Copy-Item (Join-Path $here 'install-vbcable.ps1') $vbStage -Force
|
|
$defines += "/DAudioCableStageDir=$vbStage"
|
|
# Attribution: VB-Audio's bundling grant requires we surface VB-CABLE's origin + donationware status.
|
|
Copy-Item (Join-Path $here 'licenses\VB-CABLE-NOTICE.txt') -Destination $licStage -Force
|
|
Write-Host "==> bundling VB-CABLE (virtual mic) from $VbCableDir -> $vbStage"
|
|
}
|
|
else { Write-Host "no -VbCableDir/`$env:VBCABLE_DIR -> installer built WITHOUT the bundled VB-CABLE virtual mic (CI always bundles it; see provision-windows-punktfunk-extras.ps1)" }
|
|
|
|
# --- stage the FFmpeg shared DLLs (AMD/Intel AMF/QSV build) ------------------------------------
|
|
# A host built with --features amf-qsv link-imports avcodec/avutil/swscale/... so the shared DLLs
|
|
# MUST sit next to the exe (it won't start otherwise). Bundle them from $FfmpegDir\bin - the same
|
|
# BtbN lgpl-shared tree the build linked against. A nvenc/software-only build doesn't import them, so
|
|
# this is a harmless extra there; skipped entirely when $FfmpegDir is unset.
|
|
$ffmpegBinSrc = if ($FfmpegDir) { Join-Path $FfmpegDir 'bin' } else { $null }
|
|
if ($ffmpegBinSrc -and (Test-Path $ffmpegBinSrc)) {
|
|
$dlls = Get-ChildItem -Path $ffmpegBinSrc -Filter '*.dll' -ErrorAction SilentlyContinue
|
|
if ($dlls) {
|
|
$ffmpegStage = Join-Path $OutDir 'ffmpeg'
|
|
New-Item -ItemType Directory -Force -Path $ffmpegStage | Out-Null
|
|
$dlls | ForEach-Object { Copy-Item $_.FullName -Destination $ffmpegStage -Force }
|
|
$defines += "/DFfmpegBin=$ffmpegStage"
|
|
Write-Host "bundling $($dlls.Count) FFmpeg DLL(s) from $ffmpegBinSrc"
|
|
# LGPL compliance: add FFmpeg's own license text (preserved in the BtbN tree root) + our
|
|
# attribution notice to the {app}\licenses payload so the conveyed installer carries the
|
|
# LGPLv2.1+ terms. FFmpeg is linked dynamically (separate, user-replaceable DLLs), which
|
|
# satisfies the LGPL relink requirement.
|
|
Copy-Item (Join-Path $here 'licenses\FFmpeg-LGPL-NOTICE.txt') -Destination $licStage -Force -ErrorAction SilentlyContinue
|
|
foreach ($lic in @('LICENSE.txt', 'LICENSE', 'COPYING.LGPLv2.1', 'COPYING.LGPLv3', 'COPYING.txt')) {
|
|
$p = Join-Path $FfmpegDir $lic
|
|
if (Test-Path $p) { Copy-Item $p -Destination (Join-Path $licStage "FFmpeg-$lic") -Force }
|
|
}
|
|
Write-Host "added FFmpeg license/notice to $licStage"
|
|
}
|
|
}
|
|
else { Write-Host "no FFMPEG_DIR\bin -> installer built WITHOUT FFmpeg DLLs (nvenc/software-only host)" }
|
|
|
|
# --- stage the bun runtime + the two bun payloads (web console, plugin/script runner) --------------
|
|
# Both the web console and the runner run on bun. Stage everything ISCC reads into $OutDir (the
|
|
# non-WOW64-redirected C:\t area, same reason as the .iss/host.env staging above). bun is staged ONCE
|
|
# and shared: the two payloads pass their own defines and the .iss keys WithWeb / WithScripting on
|
|
# (their dir + BunExe). Each payload is omitted when its inputs are unset (e.g. a local debug pack).
|
|
$haveBun = $BunExe -and (Test-Path $BunExe)
|
|
$wantWeb = $WebDir -and (Test-Path $WebDir) -and $haveBun
|
|
$wantScripting = $ScriptingBundle -and (Test-Path $ScriptingBundle) -and $haveBun
|
|
if ($wantWeb -or $wantScripting) {
|
|
$bunStage = Join-Path $OutDir 'bun.exe'
|
|
Copy-Item -LiteralPath $BunExe -Destination $bunStage -Force
|
|
$defines += "/DBunExe=$bunStage"
|
|
}
|
|
# The web console: the self-contained .output tree (Nitro noExternals - deps bundled + tree-shaken,
|
|
# no node_modules), run by the PunktfunkWeb scheduled task, auto-wired to the host's loopback mgmt API.
|
|
if ($wantWeb) {
|
|
$webStage = Join-Path $OutDir 'web'
|
|
if (Test-Path $webStage) { Remove-Item $webStage -Recurse -Force }
|
|
New-Item -ItemType Directory -Force -Path $webStage | Out-Null
|
|
Copy-Item (Join-Path $WebDir '*') -Destination $webStage -Recurse -Force
|
|
$webRun = Join-Path $OutDir 'web-run.cmd'
|
|
Copy-Item (Join-Path $repoRoot 'scripts\windows\web-run.cmd') -Destination $webRun -Force
|
|
# The console is provisioned by `punktfunk-host.exe web setup` (not a staged web-setup.ps1).
|
|
$defines += "/DWebDir=$webStage"
|
|
$defines += "/DWebRunCmd=$webRun"
|
|
Write-Host "bundling the web console from $WebDir (+ bun $BunExe)"
|
|
}
|
|
else { Write-Host "no -WebDir/-BunExe -> installer built WITHOUT the web console" }
|
|
# The plugin/script runner: one self-contained bundle (effect + the SDK inlined). Its scheduled task
|
|
# is registered DISABLED (opt-in) by the installer. Built by CI (SCRIPTING_BUNDLE) alongside the web
|
|
# console; omitted when -ScriptingBundle/-BunExe are unset.
|
|
if ($wantScripting) {
|
|
$scrStage = Join-Path $OutDir 'scripting'
|
|
if (Test-Path $scrStage) { Remove-Item $scrStage -Recurse -Force }
|
|
New-Item -ItemType Directory -Force -Path $scrStage | Out-Null
|
|
$scrBundle = Join-Path $scrStage 'runner-cli.js'
|
|
Copy-Item -LiteralPath $ScriptingBundle -Destination $scrBundle -Force
|
|
$scrRun = Join-Path $scrStage 'scripting-run.cmd'
|
|
Copy-Item (Join-Path $repoRoot 'scripts\windows\scripting-run.cmd') -Destination $scrRun -Force
|
|
$defines += "/DScriptingBundle=$scrBundle"
|
|
$defines += "/DScriptingRunCmd=$scrRun"
|
|
Write-Host "bundling the plugin/script runner from $ScriptingBundle (+ bun $BunExe)"
|
|
}
|
|
else { Write-Host "no -ScriptingBundle/-BunExe -> installer built WITHOUT the plugin/script runner" }
|
|
|
|
# --- build + stage the HDR Vulkan layer (pf-vkhdr-layer) --------------------------------------
|
|
# A tiny always-on Vulkan implicit layer (cdylib) that advertises HDR10/scRGB surface formats on the
|
|
# virtual display so Vulkan games (Doom: The Dark Ages, etc.) can enable HDR while streaming - the
|
|
# NVIDIA/AMD ICDs hide HDR formats on an indirect display even though they accept+present a forced HDR
|
|
# swapchain there. Self-gated on the display's actual advanced-color state, so it's a no-op on SDR.
|
|
# Standalone crate (own [workspace]); built here and registered by the installer. Skipped if cargo
|
|
# is unavailable or the build fails -> installer is produced WITHOUT the layer (non-fatal).
|
|
$layerSrc = Join-Path $here 'pf-vkhdr-layer'
|
|
if (Test-Path (Join-Path $layerSrc 'Cargo.toml')) {
|
|
$layerTarget = Join-Path $OutDir 'vklayer-target'
|
|
Write-Host "==> building pf-vkhdr-layer (cdylib)"
|
|
$prevTarget = $env:CARGO_TARGET_DIR
|
|
$env:CARGO_TARGET_DIR = $layerTarget
|
|
Push-Location $layerSrc
|
|
& cargo build --release
|
|
$layerExit = $LASTEXITCODE
|
|
Pop-Location
|
|
if ($prevTarget) { $env:CARGO_TARGET_DIR = $prevTarget } else { Remove-Item Env:\CARGO_TARGET_DIR -ErrorAction SilentlyContinue }
|
|
$layerDll = Join-Path $layerTarget 'release\pf_vkhdr_layer.dll'
|
|
if ($layerExit -eq 0 -and (Test-Path $layerDll)) {
|
|
$layerStage = Join-Path $OutDir 'vklayer'
|
|
New-Item -ItemType Directory -Force -Path $layerStage | Out-Null
|
|
Copy-Item $layerDll (Join-Path $layerStage 'pf_vkhdr_layer.dll') -Force
|
|
Copy-Item (Join-Path $layerSrc 'pf_vkhdr_layer.json') (Join-Path $layerStage 'pf_vkhdr_layer.json') -Force
|
|
Sign-File (Join-Path $layerStage 'pf_vkhdr_layer.dll')
|
|
$defines += "/DVkLayerDir=$layerStage"
|
|
Write-Host "==> staged pf-vkhdr-layer -> $layerStage"
|
|
}
|
|
else { Write-Warning "pf-vkhdr-layer build failed ($layerExit) - installer built WITHOUT the HDR Vulkan layer" }
|
|
}
|
|
else { Write-Host "no pf-vkhdr-layer crate -> installer built WITHOUT the HDR Vulkan layer" }
|
|
|
|
# --- build the installer (from the non-redirected copy under C:\t) -----------------------------
|
|
Write-Host "==> ISCC $($defines -join ' ') $issLocal"
|
|
& $iscc @defines $issLocal
|
|
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 }
|
|
}
|