feat(host/windows,packaging): installer overhaul - branding, VB-CABLE, GameStream choice, driver uninstall
ci / docs-site (push) Successful in 1m3s
android / android (push) Successful in 3m34s
decky / build-publish (push) Successful in 11s
apple / swift (push) Successful in 1m7s
ci / rust (push) Successful in 1m36s
ci / web (push) Successful in 49s
apple / screenshots (push) Successful in 5m20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
windows-host / package (push) Successful in 6m41s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
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 5s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m17s
ci / bench (push) Successful in 4m41s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m22s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m37s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m8s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m13s
docker / deploy-docs (push) Successful in 16s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m0s
deb / build-publish (push) Successful in 3m6s

- Modern branded wizard: WizardStyle=modern dynamic windows11 (Inno >= 6.6,
  plain-modern fallback for older compilers; CI provisioning upgrades a
  pre-6.6 Inno). Brand-mark wizard side panels + header tiles (100-200% DPI)
  and a multi-size punktfunk.ico (SetupIconFile + Apps & Features), generated
  AND committed by branding/gen-branding.ps1 from the canonical brand geometry.
  Gotcha encoded in the script: ISCC rejects all-PNG icons, so entries <= 64px
  are classic DIBs (PNG only at 128/256), and the ICO is load-verified.

- VB-CABLE actually ships now: windows-host.yml never set VBCABLE_DIR, so every
  published installer silently omitted the virtual mic (broken mic passthrough
  in the field). CI provisions the pinned, SHA-256-verified official Pack45
  (provision-windows-punktfunk-extras.ps1) and the pack now FAILS on a
  supplied-but-invalid dir instead of shipping mic-less again. Attribution per
  VB-Audio's bundling grant surfaced in the visible wizard task text (vendor,
  vb-cable.com, donationware) on top of the licenses notice.

- GameStream (Moonlight) compat is a wizard task (checked by default) ->
  service install --gamestream=on|off writes PUNKTFUNK_HOST_CMD=
  serve[ --gamestream] into host.env. Only the two canonical values are ever
  rewritten - a hand-customized command line survives upgrades. Silent
  installs: /MERGETASKS="!gamestream".

- Driver uninstall (field report: our virtual-device drivers survived
  uninstall): new `driver uninstall [--gamepad]` removes the pf-vdisplay
  device node(s) + the pf-vdisplay/pf-dualsense/pf-xusb driver-store packages,
  wired into [UninstallRun] after service uninstall. Locale-safe by
  construction: devices matched on unlocalized VALUES (never pnputil's
  localized labels), packages found by INF content scan - validated against a
  German-locale box ("Instanz-ID:" parse; 7/7 punktfunk INFs matched, no
  foreign hits). VB-CABLE is deliberately left installed (shared third-party
  component with its own uninstaller).

Installer compile, cargo check/clippy/fmt, and the ASCII locale gate are green;
the wizard look + uninstall flow still need one on-glass pass on a disposable
box (this box runs the live host).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 12:16:11 +02:00
parent 9074781acd
commit f48dc5dfce
20 changed files with 568 additions and 29 deletions
+68 -3
View File
@@ -87,7 +87,7 @@ fn event_handle(ev: &OnceLock<OwnedHandle>) -> Option<HANDLE> {
pub fn main(args: &[String]) -> Result<()> {
match args.first().map(String::as_str) {
Some("run") => run(),
Some("install") => install(),
Some("install") => install(&args[1..]),
Some("uninstall") => uninstall(),
Some("start") => sc(&["start", SERVICE_NAME]),
Some("stop") => sc(&["stop", SERVICE_NAME]),
@@ -96,7 +96,9 @@ pub fn main(args: &[String]) -> Result<()> {
eprintln!(
"punktfunk-host service — Windows service control\n\n\
USAGE:\n\
\x20 punktfunk-host service install register the auto-start service + firewall rules\n\
\x20 punktfunk-host service install [--gamestream=on|off]\n\
\x20 register the auto-start service + firewall rules\n\
\x20 (--gamestream sets host.env's PUNKTFUNK_HOST_CMD)\n\
\x20 punktfunk-host service uninstall stop + remove the service + firewall rules\n\
\x20 punktfunk-host service start start the service now\n\
\x20 punktfunk-host service stop stop the service\n\
@@ -606,12 +608,20 @@ unsafe fn open_log_handle(path: &std::path::Path) -> Result<HANDLE> {
// ── install / uninstall ──────────────────────────────────────────────────────────────────────────
fn install() -> Result<()> {
fn install(args: &[String]) -> Result<()> {
use windows_service::service::{
ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceType,
};
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
// `--gamestream=on|off` (the installer's wizard task): None = flag absent, keep host.env as-is.
let gamestream = match args.iter().find_map(|a| a.strip_prefix("--gamestream=")) {
Some("on") => Some(true),
Some("off") => Some(false),
Some(v) => bail!("--gamestream must be 'on' or 'off' (got '{v}')"),
None => None,
};
let exe = std::env::current_exe().context("current_exe")?;
let manager = ServiceManager::local_computer(
None::<&str>,
@@ -653,6 +663,9 @@ fn install() -> Result<()> {
}
ensure_default_host_env()?;
if let Some(on) = gamestream {
apply_gamestream_choice(on);
}
add_firewall_rules();
println!(
@@ -721,6 +734,58 @@ fn ensure_default_host_env() -> Result<()> {
Ok(())
}
/// Write the installer's GameStream choice into host.env's `PUNKTFUNK_HOST_CMD`. Upgrade-safe:
/// only an absent line or one of the two canonical values (`serve` / `serve --gamestream`) is
/// rewritten — a hand-customized command line is the user's, and stays. Best-effort (warns).
fn apply_gamestream_choice(enable: bool) {
let path = host_env_path();
let desired = if enable {
"serve --gamestream"
} else {
"serve"
};
let Ok(text) = std::fs::read_to_string(&path) else {
eprintln!(
"warning: could not read {} to apply the GameStream choice",
path.display()
);
return;
};
let mut lines: Vec<String> = text.lines().map(str::to_string).collect();
let current = lines.iter().position(|l| {
let t = l.trim_start();
!t.starts_with('#') && t.starts_with("PUNKTFUNK_HOST_CMD=")
});
match current {
Some(i) => {
let value = lines[i].trim_start()["PUNKTFUNK_HOST_CMD=".len()..].trim();
if value == desired {
return; // already what the installer chose
}
if value != "serve" && value != "serve --gamestream" {
println!(
"host.env has a customized PUNKTFUNK_HOST_CMD ({value}) - leaving it \
(installer GameStream choice not applied)"
);
return;
}
lines[i] = format!("PUNKTFUNK_HOST_CMD={desired}");
}
None => lines.push(format!("PUNKTFUNK_HOST_CMD={desired}")),
}
let mut out = lines.join("\n");
out.push('\n');
// Rewrite through write_secret_file so the SYSTEM/Administrators DACL is re-asserted.
if let Err(e) = crate::gamestream::write_secret_file(&path, out.as_bytes()) {
eprintln!("warning: could not write {}: {e}", path.display());
return;
}
println!(
"GameStream (Moonlight) compatibility: {} (PUNKTFUNK_HOST_CMD={desired})",
if enable { "enabled" } else { "disabled" }
);
}
// ── firewall + sc helpers ────────────────────────────────────────────────────────────────────────
/// Inbound firewall rules for the streaming ports (best-effort; logs but never fails the install).