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>
@@ -93,6 +93,13 @@ jobs:
|
|||||||
if (-not $env:FFMPEG_DIR) {
|
if (-not $env:FFMPEG_DIR) {
|
||||||
"FFMPEG_DIR=C:\Users\Public\ffmpeg" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
"FFMPEG_DIR=C:\Users\Public\ffmpeg" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||||
}
|
}
|
||||||
|
# VBCABLE_DIR: the pinned official VB-CABLE package (provisioned by
|
||||||
|
# provision-windows-punktfunk-extras.ps1) -> pack-host-installer.ps1 bundles the
|
||||||
|
# streaming virtual microphone. Same daemon-env-or-fallback pattern as FFMPEG_DIR
|
||||||
|
# (the daemon env only refreshes on a runner-task restart).
|
||||||
|
if (-not $env:VBCABLE_DIR) {
|
||||||
|
"VBCABLE_DIR=C:\Users\Public\vbcable" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||||
|
}
|
||||||
$v = if ($env:GITHUB_REF -like 'refs/tags/v*') {
|
$v = if ($env:GITHUB_REF -like 'refs/tags/v*') {
|
||||||
$env:GITHUB_REF_NAME -replace '^v', ''
|
$env:GITHUB_REF_NAME -replace '^v', ''
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! `punktfunk-host driver install` / `web setup` - the install-time work the Windows installer's Inno
|
//! `punktfunk-host driver install|uninstall` / `web setup` - the install-time work the Windows
|
||||||
//! `[Run]` section delegates to the host EXE instead of locale-parsed PowerShell *files*.
|
//! installer's Inno `[Run]`/`[UninstallRun]` sections delegate to the host EXE instead of
|
||||||
|
//! locale-parsed PowerShell *files*.
|
||||||
//!
|
//!
|
||||||
//! Why: Windows PowerShell 5.1 reads a BOM-less `.ps1` *file* in the machine's ANSI codepage, so on a
|
//! Why: Windows PowerShell 5.1 reads a BOM-less `.ps1` *file* in the machine's ANSI codepage, so on a
|
||||||
//! non-English locale a stray non-ASCII byte mis-decodes and the script aborts "unterminated string" -
|
//! non-English locale a stray non-ASCII byte mis-decodes and the script aborts "unterminated string" -
|
||||||
@@ -45,11 +46,15 @@ fn run_capture(cmd: &str, args: &[&str]) -> String {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── `driver install [--gamepad] --dir <stage>` ─────────────────────────────────────────────────
|
// ── `driver install [--gamepad] --dir <stage>` / `driver uninstall [--gamepad]` ────────────────
|
||||||
pub fn driver_main(args: &[String]) -> Result<()> {
|
pub fn driver_main(args: &[String]) -> Result<()> {
|
||||||
match args.first().map(String::as_str) {
|
match args.first().map(String::as_str) {
|
||||||
Some("install") => driver_install(&args[1..]),
|
Some("install") => driver_install(&args[1..]),
|
||||||
_ => bail!("usage: punktfunk-host driver install --dir <stage> [--gamepad]"),
|
Some("uninstall") => driver_uninstall(&args[1..]),
|
||||||
|
_ => bail!(
|
||||||
|
"usage: punktfunk-host driver install --dir <stage> [--gamepad]\n\
|
||||||
|
\x20 punktfunk-host driver uninstall [--gamepad]"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +165,121 @@ fn install_gamepad(dir: &Path) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── `driver uninstall [--gamepad]` ──────────────────────────────────────────────────────────────
|
||||||
|
// The uninstaller's cleanup counterpart (Inno [UninstallRun]) — the field report was that our
|
||||||
|
// virtual-device drivers survived an uninstall. Removes the pf-vdisplay device node(s) + driver
|
||||||
|
// package, or (--gamepad) the pf-dualsense/pf-xusb driver packages (their devnodes are per-session
|
||||||
|
// SwDeviceCreate'd and are already gone once the service stopped). Locale-safe by construction: we
|
||||||
|
// never parse pnputil's localized LABELS — devices are matched on the un-localized VALUE side
|
||||||
|
// (instance IDs / device IDs), and driver packages are found by scanning %WINDIR%\INF\oem*.inf
|
||||||
|
// CONTENT for our driver names, then passed to pnputil by file name.
|
||||||
|
|
||||||
|
fn driver_uninstall(args: &[String]) -> Result<()> {
|
||||||
|
let gamepad = flag_present(args, "--gamepad");
|
||||||
|
let (what, res) = if gamepad {
|
||||||
|
("gamepad", uninstall_gamepad())
|
||||||
|
} else {
|
||||||
|
("pf-vdisplay", uninstall_pf_vdisplay())
|
||||||
|
};
|
||||||
|
if let Err(e) = res {
|
||||||
|
// Same best-effort contract as install: never abort the (un)installer over a driver.
|
||||||
|
eprintln!("warning: {what} driver uninstall: {e:#}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uninstall_pf_vdisplay() -> Result<()> {
|
||||||
|
// 1. Remove the ROOT device node(s) the installer created via nefconc (leaving them would keep
|
||||||
|
// a ghost "punktfunk virtual display" in Device Manager forever — the exact complaint).
|
||||||
|
for id in pf_vdisplay_instance_ids() {
|
||||||
|
if run_quiet("pnputil", &["/remove-device", &id]) {
|
||||||
|
println!("removed device node {id}");
|
||||||
|
} else {
|
||||||
|
eprintln!("warning: pnputil /remove-device {id} failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. Delete the driver package from the driver store.
|
||||||
|
delete_store_drivers(&["pf_vdisplay"]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uninstall_gamepad() -> Result<()> {
|
||||||
|
delete_store_drivers(&["pf_dualsense", "pf_dualshock4", "pf_xusb"]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Instance IDs of enumerated punktfunk virtual-display devices. Parses `pnputil /enum-devices`
|
||||||
|
/// per-device blocks (blank-line separated); a block is ours if it mentions the pf_vdisplay
|
||||||
|
/// hardware id / description, and its instance ID is the first line's VALUE (never the localized
|
||||||
|
/// label) — pnputil prints "Instance ID:" (or its translation) first in every block.
|
||||||
|
fn pf_vdisplay_instance_ids() -> Vec<String> {
|
||||||
|
let out = run_capture("pnputil", &["/enum-devices", "/class", "Display"]);
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
for block in out.split("\r\n\r\n").flat_map(|b| b.split("\n\n")) {
|
||||||
|
let lo = block.to_ascii_lowercase();
|
||||||
|
if !lo.contains("pf_vdisplay") && !lo.contains("punktfunk virtual display") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(first) = block.lines().find(|l| !l.trim().is_empty()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some((_, value)) = first.split_once(':') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let id = value.trim();
|
||||||
|
// Sanity: an instance ID is a backslashed path with no spaces (e.g. ROOT\DISPLAY\0000).
|
||||||
|
if !id.is_empty() && id.contains('\\') && !id.contains(' ') {
|
||||||
|
ids.push(id.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ids
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete every driver-store package (`%WINDIR%\INF\oem*.inf`) whose INF text mentions one of
|
||||||
|
/// `needles` — our driver names are unique enough that a content match identifies the package
|
||||||
|
/// without parsing `pnputil /enum-drivers`' localized output. `/uninstall /force` also unbinds it
|
||||||
|
/// from any remaining devnodes.
|
||||||
|
fn delete_store_drivers(needles: &[&str]) {
|
||||||
|
let windir = std::env::var("WINDIR").unwrap_or_else(|_| r"C:\Windows".into());
|
||||||
|
let inf_dir = Path::new(&windir).join("INF");
|
||||||
|
let Ok(entries) = std::fs::read_dir(&inf_dir) else {
|
||||||
|
eprintln!("warning: cannot read {}", inf_dir.display());
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for path in entries.flatten().map(|e| e.path()) {
|
||||||
|
let name = file_name(&path).to_ascii_lowercase();
|
||||||
|
if !name.starts_with("oem") || !name.ends_with(".inf") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let text = read_inf_text(&path).to_ascii_lowercase();
|
||||||
|
if !needles.iter().any(|n| text.contains(n)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if run_quiet(
|
||||||
|
"pnputil",
|
||||||
|
&["/delete-driver", &name, "/uninstall", "/force"],
|
||||||
|
) {
|
||||||
|
println!("deleted driver package {name}");
|
||||||
|
} else {
|
||||||
|
eprintln!("warning: pnputil /delete-driver {name} /uninstall /force failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// INF files in %WINDIR%\INF are ANSI or UTF-16LE(+BOM); decode either so content matching works.
|
||||||
|
fn read_inf_text(path: &Path) -> String {
|
||||||
|
let bytes = std::fs::read(path).unwrap_or_default();
|
||||||
|
if bytes.len() >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE {
|
||||||
|
let units: Vec<u16> = bytes[2..]
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
||||||
|
.collect();
|
||||||
|
String::from_utf16_lossy(&units)
|
||||||
|
} else {
|
||||||
|
String::from_utf8_lossy(&bytes).into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Is a punktfunk virtual-display device already enumerated? Matches the device ID / description, which
|
/// Is a punktfunk virtual-display device already enumerated? Matches the device ID / description, which
|
||||||
/// are NOT localized, so the substring check is locale-safe.
|
/// are NOT localized, so the substring check is locale-safe.
|
||||||
fn pf_vdisplay_present() -> bool {
|
fn pf_vdisplay_present() -> bool {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ fn event_handle(ev: &OnceLock<OwnedHandle>) -> Option<HANDLE> {
|
|||||||
pub fn main(args: &[String]) -> Result<()> {
|
pub fn main(args: &[String]) -> Result<()> {
|
||||||
match args.first().map(String::as_str) {
|
match args.first().map(String::as_str) {
|
||||||
Some("run") => run(),
|
Some("run") => run(),
|
||||||
Some("install") => install(),
|
Some("install") => install(&args[1..]),
|
||||||
Some("uninstall") => uninstall(),
|
Some("uninstall") => uninstall(),
|
||||||
Some("start") => sc(&["start", SERVICE_NAME]),
|
Some("start") => sc(&["start", SERVICE_NAME]),
|
||||||
Some("stop") => sc(&["stop", SERVICE_NAME]),
|
Some("stop") => sc(&["stop", SERVICE_NAME]),
|
||||||
@@ -96,7 +96,9 @@ pub fn main(args: &[String]) -> Result<()> {
|
|||||||
eprintln!(
|
eprintln!(
|
||||||
"punktfunk-host service — Windows service control\n\n\
|
"punktfunk-host service — Windows service control\n\n\
|
||||||
USAGE:\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 uninstall stop + remove the service + firewall rules\n\
|
||||||
\x20 punktfunk-host service start start the service now\n\
|
\x20 punktfunk-host service start start the service now\n\
|
||||||
\x20 punktfunk-host service stop stop the service\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 ──────────────────────────────────────────────────────────────────────────
|
// ── install / uninstall ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn install() -> Result<()> {
|
fn install(args: &[String]) -> Result<()> {
|
||||||
use windows_service::service::{
|
use windows_service::service::{
|
||||||
ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceType,
|
ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceType,
|
||||||
};
|
};
|
||||||
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
|
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 exe = std::env::current_exe().context("current_exe")?;
|
||||||
let manager = ServiceManager::local_computer(
|
let manager = ServiceManager::local_computer(
|
||||||
None::<&str>,
|
None::<&str>,
|
||||||
@@ -653,6 +663,9 @@ fn install() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensure_default_host_env()?;
|
ensure_default_host_env()?;
|
||||||
|
if let Some(on) = gamestream {
|
||||||
|
apply_gamestream_choice(on);
|
||||||
|
}
|
||||||
add_firewall_rules();
|
add_firewall_rules();
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
@@ -721,6 +734,58 @@ fn ensure_default_host_env() -> Result<()> {
|
|||||||
Ok(())
|
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 ────────────────────────────────────────────────────────────────────────
|
// ── firewall + sc helpers ────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Inbound firewall rules for the streaming ports (best-effort; logs but never fails the install).
|
/// Inbound firewall rules for the streaming ports (best-effort; logs but never fails the install).
|
||||||
|
|||||||
@@ -47,16 +47,31 @@ parse breakage that silently failed installs on non-English boxes.
|
|||||||
**`PunktfunkWeb`** scheduled task (boot, SYSTEM, restart-on-failure → `web-run.cmd` → `bun` on
|
**`PunktfunkWeb`** scheduled task (boot, SYSTEM, restart-on-failure → `web-run.cmd` → `bun` on
|
||||||
`:3000`), opens TCP 3000, and starts it. It proxies the host's loopback mgmt API with the host's
|
`:3000`), opens TCP 3000, and starts it. It proxies the host's loopback mgmt API with the host's
|
||||||
own `%ProgramData%\punktfunk\mgmt-token`.
|
own `%ProgramData%\punktfunk\mgmt-token`.
|
||||||
|
- **GameStream (Moonlight) compatibility is a wizard task** (checked by default): the choice is passed
|
||||||
|
to `service install --gamestream=on|off`, which writes `PUNKTFUNK_HOST_CMD=serve --gamestream` (or
|
||||||
|
`serve`, the secure native-only host) into `host.env`. Upgrade-safe: a hand-customized
|
||||||
|
`PUNKTFUNK_HOST_CMD` is never overwritten.
|
||||||
|
- **Branded, modern wizard**: `WizardStyle=modern dynamic windows11` (Inno ≥ 6.6 — Windows-11-style
|
||||||
|
controls following the system light/dark theme; pre-6.6 compilers fall back to plain `modern`), with
|
||||||
|
the punktfunk lens mark on the side panel / header tile and a multi-size `punktfunk.ico`
|
||||||
|
(`SetupIconFile` + the Apps & features entry). Assets are generated **and committed** by
|
||||||
|
`branding/gen-branding.ps1` from the canonical brand geometry (`web/src/components/brand-mark.tsx`);
|
||||||
|
re-run it only when the brand changes.
|
||||||
- **Upgrade:** stops a running `PunktfunkHost` service and waits for `STOPPED` before replacing files
|
- **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;
|
(otherwise the locked exe / respawning supervisor would block the copy), then re-points the service;
|
||||||
the existing console password is kept (the wizard page is skipped).
|
the existing console password is kept (the wizard page is skipped).
|
||||||
- **Uninstall** (Add/Remove Programs): runs `service uninstall` (stop + delete service + remove
|
- **Uninstall** (Add/Remove Programs): runs `service uninstall` (stop + delete service + remove
|
||||||
firewall rules) and removes the `PunktfunkWeb` task + its firewall rule. The pf-vdisplay driver and the
|
firewall rules), removes the `PunktfunkWeb` task + its firewall rule, then `driver uninstall` (+
|
||||||
`%ProgramData%\punktfunk` config (incl. `web-password`) are intentionally left in place.
|
`--gamepad`) removes the punktfunk virtual-device drivers — the pf-vdisplay device node(s) and the
|
||||||
|
pf-vdisplay / pf-dualsense / pf-xusb driver-store packages (the field report was that they survived
|
||||||
|
uninstall). **VB-CABLE is intentionally NOT removed** (a third-party shared component the user may
|
||||||
|
use elsewhere — its own uninstaller is `VBCABLE_Setup_x64.exe -u -h`); the `%ProgramData%\punktfunk`
|
||||||
|
config (incl. `web-password`) is also left in place.
|
||||||
|
|
||||||
Silent install: `punktfunk-host-setup-<ver>.exe /VERYSILENT` (omit the driver with
|
Silent install: `punktfunk-host-setup-<ver>.exe /VERYSILENT` (omit the driver with
|
||||||
`/MERGETASKS="!installdriver"`). A silent fresh install uses the generated random console password —
|
`/MERGETASKS="!installdriver"`; disable Moonlight compat with `/MERGETASKS="!gamestream"`). A silent
|
||||||
read it from `%ProgramData%\punktfunk\web-password`.
|
fresh install uses the generated random console password — read it from
|
||||||
|
`%ProgramData%\punktfunk\web-password`.
|
||||||
|
|
||||||
## Prerequisites on the target box
|
## Prerequisites on the target box
|
||||||
|
|
||||||
@@ -70,18 +85,24 @@ read it from `%ProgramData%\punktfunk\web-password`.
|
|||||||
Output` capture endpoint surfaces as a host mic. A Windows audio device can only be created by a
|
Output` capture endpoint surfaces as a host mic. A Windows audio device can only be created by a
|
||||||
**kernel-mode** driver (no UMDF path exists), so unlike our self-signed UMDF drivers we cannot ship our
|
**kernel-mode** driver (no UMDF path exists), so unlike our self-signed UMDF drivers we cannot ship our
|
||||||
own — VB-CABLE is a vendor-signed cable that loads with no test-signing. It is **donationware** by
|
own — VB-CABLE is a vendor-signed cable that loads with no test-signing. It is **donationware** by
|
||||||
VB-Audio, redistributed under VB-Audio's bundling grant (only the single base cable); see
|
VB-Audio, redistributed under VB-Audio's bundling grant (only the single base cable) — the grant
|
||||||
`licenses/VB-CABLE-NOTICE.txt`. The package binary is **not** in the repo — supply it to the packer via
|
requires the end user to see VB-CABLE's origin + donationware status, which the wizard task text and
|
||||||
`-VbCableDir` / `$env:VBCABLE_DIR` (the extracted official package, containing `VBCABLE_Setup_x64.exe`).
|
`licenses/VB-CABLE-NOTICE.txt` surface. The package binary is **not** in the repo — CI provisions the
|
||||||
Absent → the installer is built without it and the host falls back to auto-installing the Steam
|
**pinned, SHA-256-verified official package** onto the runner (`scripts/ci/provision-windows-punktfunk-extras.ps1`
|
||||||
Streaming pair. *(Endgame: attestation-sign our own MIT virtual-audio driver to drop this dependency.)*
|
→ `C:\Users\Public\vbcable`) and `windows-host.yml` passes it via `$env:VBCABLE_DIR`, so **published
|
||||||
|
installers always bundle it**; locally supply `-VbCableDir` / `$env:VBCABLE_DIR` (the extracted
|
||||||
|
official package, containing `VBCABLE_Setup_x64.exe`). Unset → the installer is built without it and
|
||||||
|
the host falls back to auto-installing the Steam Streaming pair; set-but-invalid → the pack **fails**
|
||||||
|
(a broken provisioning must not silently ship a mic-less installer again). *(Endgame:
|
||||||
|
attestation-sign our own MIT virtual-audio driver to drop this dependency.)*
|
||||||
|
|
||||||
## Files here
|
## Files here
|
||||||
|
|
||||||
| File | Role |
|
| File | Role |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `punktfunk-host.iss` | Inno Setup script (the installer definition). |
|
| `punktfunk-host.iss` | Inno Setup script (the installer definition). |
|
||||||
| `pack-host-installer.ps1` | Orchestrator: cert + sign exe, **build + sign the drivers from source**, stage them + FFmpeg + the **web console** (`.output` + bun) + the HDR layer, run ISCC, sign setup.exe. |
|
| `branding/` | Wizard branding: `gen-branding.ps1` renders the brand mark into the committed `wizard-image-*.bmp` / `wizard-small-*.bmp` (100–200% DPI) + `punktfunk.ico`. Re-run only on a brand change. |
|
||||||
|
| `pack-host-installer.ps1` | Orchestrator: cert + sign exe, **build + sign the drivers from source**, stage them + FFmpeg + VB-CABLE + the **web console** (`.output` + bun) + the HDR layer + branding, run ISCC, sign setup.exe. |
|
||||||
| `build-pf-vdisplay.ps1` | Build pf-vdisplay from source (the `drivers/` workspace) + clear FORCE_INTEGRITY + sign `.dll`/`.cat` + export `.cer`. |
|
| `build-pf-vdisplay.ps1` | Build pf-vdisplay from source (the `drivers/` workspace) + clear FORCE_INTEGRITY + sign `.dll`/`.cat` + export `.cer`. |
|
||||||
| `build-gamepad-drivers.ps1` | Sign + catalog the gamepad drivers (`pf-dualsense` + `pf-xusb`) from the same workspace build (`-SkipBuild`), one shared cert. |
|
| `build-gamepad-drivers.ps1` | Sign + catalog the gamepad drivers (`pf-dualsense` + `pf-xusb`) from the same workspace build (`-SkipBuild`), one shared cert. |
|
||||||
| `install-vbcable.ps1` | On-target: seed VB-Audio's cert into `TrustedPublisher`, silently install the bundled VB-CABLE (`-i -h`). Run by the installer's *Install VB-CABLE virtual audio* task; idempotent + always exits 0 (non-fatal). |
|
| `install-vbcable.ps1` | On-target: seed VB-Audio's cert into `TrustedPublisher`, silently install the bundled VB-CABLE (`-i -h`). Run by the installer's *Install VB-CABLE virtual audio* task; idempotent + always exits 0 (non-fatal). |
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generate the punktfunk host installer branding assets (wizard BMPs + setup .ico).
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Renders the punktfunk brand mark - the two overlapping circles ("lens") from
|
||||||
|
web/src/components/brand-mark.tsx (the canonical flattened geometry, shared with the Apple icon,
|
||||||
|
the marketing site and the docs) - into the assets Inno Setup consumes:
|
||||||
|
|
||||||
|
wizard-image-*.bmp welcome/finish page side panel (164x314 base, 100..200% DPI variants);
|
||||||
|
dark violet gradient panel + the mark + the lowercase wordmark. The panel
|
||||||
|
is self-contained dark, so it reads correctly in BOTH the light and dark
|
||||||
|
(WizardStyle=dynamic) wizard appearances.
|
||||||
|
wizard-small-*.bmp header tile on the inner pages (55x55 base, 100..200% DPI variants);
|
||||||
|
the square brand tile (mark on #1C1530), matching the MSIX client tile.
|
||||||
|
punktfunk.ico multi-size icon (16..256, PNG-compressed entries - Vista+ format, we
|
||||||
|
require Windows 10) for SetupIconFile + the Apps & Features entry.
|
||||||
|
|
||||||
|
Outputs are COMMITTED next to this script (like include/punktfunk_core.h, generated-but-checked-in);
|
||||||
|
re-run only when the brand changes. Everything is drawn 4x supersampled and downscaled
|
||||||
|
(System.Drawing regions/clips do not antialias), so edges stay clean at every size.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
pwsh -File packaging/windows/branding/gen-branding.ps1
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param([string]$OutDir = $PSScriptRoot)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
|
||||||
|
# --- brand constants (colors from brand-mark.tsx; tile background from the MSIX assets) -------
|
||||||
|
$colLight = [System.Drawing.Color]::FromArgb(255, 0xA7, 0x9F, 0xF8) # large circle
|
||||||
|
$colDeep = [System.Drawing.Color]::FromArgb(255, 0x6C, 0x5B, 0xF3) # small circle
|
||||||
|
$colHi = [System.Drawing.Color]::FromArgb(255, 0xD2, 0xC9, 0xFB) # lens overlap highlight
|
||||||
|
$colTile = [System.Drawing.Color]::FromArgb(255, 0x1C, 0x15, 0x30) # brand tile background
|
||||||
|
$colPanelTop = [System.Drawing.Color]::FromArgb(255, 0x27, 0x1E, 0x46) # wizard panel gradient
|
||||||
|
$colPanelBot = [System.Drawing.Color]::FromArgb(255, 0x11, 0x0D, 0x1F)
|
||||||
|
$colText = [System.Drawing.Color]::FromArgb(255, 0xEA, 0xE6, 0xFB) # wordmark on the panel
|
||||||
|
|
||||||
|
# Mark geometry in the 1000-unit viewbox of brand-mark.tsx: two r=194.41 circles at (403.04,597.26)
|
||||||
|
# (light, behind) and (597.81,402.85) (deep, in front), their intersection filled as the highlight.
|
||||||
|
$R = 194.41
|
||||||
|
$c1x = 403.037; $c1y = 597.262
|
||||||
|
$c2x = 597.8075; $c2y = 402.8525
|
||||||
|
# Mark bounding box -> center/span, so callers can place it by center + size.
|
||||||
|
$bbMinX = $c1x - $R; $bbMaxX = $c2x + $R
|
||||||
|
$bbMinY = $c2y - $R; $bbMaxY = $c1y + $R
|
||||||
|
$markCx = ($bbMinX + $bbMaxX) / 2.0
|
||||||
|
$markCy = ($bbMinY + $bbMaxY) / 2.0
|
||||||
|
$markSpan = $bbMaxX - $bbMinX # == $bbMaxY - $bbMinY (the bbox is square)
|
||||||
|
|
||||||
|
# Draw the mark onto $g centered at ($cx,$cy) with bounding-box size $size (device pixels).
|
||||||
|
function Draw-Mark([System.Drawing.Graphics]$g, [double]$cx, [double]$cy, [double]$size) {
|
||||||
|
$s = $size / $markSpan
|
||||||
|
function ellRect([double]$ecx, [double]$ecy) {
|
||||||
|
$r = $R * $s
|
||||||
|
[System.Drawing.RectangleF]::new(
|
||||||
|
[float]($cx + ($ecx - $markCx) * $s - $r), [float]($cy + ($ecy - $markCy) * $s - $r),
|
||||||
|
[float](2 * $r), [float](2 * $r))
|
||||||
|
}
|
||||||
|
$r1 = ellRect $c1x $c1y
|
||||||
|
$r2 = ellRect $c2x $c2y
|
||||||
|
$b = New-Object System.Drawing.SolidBrush($colLight)
|
||||||
|
$g.FillEllipse($b, $r1); $b.Dispose()
|
||||||
|
$b = New-Object System.Drawing.SolidBrush($colDeep)
|
||||||
|
$g.FillEllipse($b, $r2); $b.Dispose()
|
||||||
|
# Highlight = intersection: clip to circle 1, fill circle 2. The clip edge is not antialiased,
|
||||||
|
# but every caller renders 4x supersampled and downscales, which smooths it.
|
||||||
|
$p1 = New-Object System.Drawing.Drawing2D.GraphicsPath
|
||||||
|
$p1.AddEllipse($r1)
|
||||||
|
$g.SetClip($p1)
|
||||||
|
$b = New-Object System.Drawing.SolidBrush($colHi)
|
||||||
|
$g.FillEllipse($b, $r2); $b.Dispose()
|
||||||
|
$g.ResetClip(); $p1.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
# New 32bpp canvas + antialiased Graphics.
|
||||||
|
function New-Canvas([int]$w, [int]$h) {
|
||||||
|
$bmp = New-Object System.Drawing.Bitmap($w, $h, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||||
|
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||||
|
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
|
||||||
|
$g.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAlias
|
||||||
|
@($bmp, $g)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Downscale $src to $w x $h (high-quality bicubic) - the supersample resolve.
|
||||||
|
function Resize-Bitmap([System.Drawing.Bitmap]$src, [int]$w, [int]$h) {
|
||||||
|
$dst = New-Object System.Drawing.Bitmap($w, $h, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||||
|
$g = [System.Drawing.Graphics]::FromImage($dst)
|
||||||
|
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
||||||
|
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
||||||
|
$g.DrawImage($src, (New-Object System.Drawing.Rectangle(0, 0, $w, $h)),
|
||||||
|
0, 0, $src.Width, $src.Height, [System.Drawing.GraphicsUnit]::Pixel)
|
||||||
|
$g.Dispose()
|
||||||
|
$dst
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save as 24bpp BMP (opaque - what Inno's wizard image loader expects by default).
|
||||||
|
function Save-Bmp24([System.Drawing.Bitmap]$bmp, [string]$path) {
|
||||||
|
$b24 = $bmp.Clone((New-Object System.Drawing.Rectangle(0, 0, $bmp.Width, $bmp.Height)),
|
||||||
|
[System.Drawing.Imaging.PixelFormat]::Format24bppRgb)
|
||||||
|
$b24.Save($path, [System.Drawing.Imaging.ImageFormat]::Bmp)
|
||||||
|
$b24.Dispose()
|
||||||
|
Write-Host " wrote $path ($($bmp.Width)x$($bmp.Height))"
|
||||||
|
}
|
||||||
|
|
||||||
|
$SS = 4 # supersample factor
|
||||||
|
|
||||||
|
# --- wizard side panel (welcome/finish page): gradient + mark + wordmark ----------------------
|
||||||
|
# Base size 164x314 (Inno's classic canvas); DPI variants via the wizard-image-*.bmp wildcard.
|
||||||
|
foreach ($pct in 100, 125, 150, 175, 200) {
|
||||||
|
$w = [int][Math]::Round(164 * $pct / 100.0)
|
||||||
|
$h = [int][Math]::Round(314 * $pct / 100.0)
|
||||||
|
$bmp, $g = New-Canvas ($w * $SS) ($h * $SS)
|
||||||
|
$rect = New-Object System.Drawing.Rectangle(0, 0, ($w * $SS), ($h * $SS))
|
||||||
|
$grad = New-Object System.Drawing.Drawing2D.LinearGradientBrush($rect, $colPanelTop, $colPanelBot, 90.0)
|
||||||
|
$g.FillRectangle($grad, $rect); $grad.Dispose()
|
||||||
|
# Mark: 58% of the panel width, centered horizontally, optical center at ~40% height.
|
||||||
|
Draw-Mark $g ($w * $SS / 2.0) ($h * $SS * 0.40) ($w * $SS * 0.58)
|
||||||
|
# Wordmark: lowercase brand name under the mark.
|
||||||
|
$font = New-Object System.Drawing.Font('Segoe UI Semibold', [float](13.0 * $SS * $pct / 100.0), [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Pixel)
|
||||||
|
$tb = New-Object System.Drawing.SolidBrush($colText)
|
||||||
|
$fmt = New-Object System.Drawing.StringFormat
|
||||||
|
$fmt.Alignment = [System.Drawing.StringAlignment]::Center
|
||||||
|
$g.DrawString('punktfunk', $font, $tb,
|
||||||
|
(New-Object System.Drawing.PointF([float]($w * $SS / 2.0), [float]($h * $SS * 0.60))), $fmt)
|
||||||
|
$fmt.Dispose(); $tb.Dispose(); $font.Dispose(); $g.Dispose()
|
||||||
|
$out = Resize-Bitmap $bmp $w $h
|
||||||
|
$bmp.Dispose()
|
||||||
|
Save-Bmp24 $out (Join-Path $OutDir ("wizard-image-{0}.bmp" -f $pct))
|
||||||
|
$out.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- wizard header tile (inner pages): the square brand tile --------------------------------
|
||||||
|
# Base size 55x55; DPI variants via the wizard-small-*.bmp wildcard. Opaque square (BMP has no
|
||||||
|
# alpha here): the same full-bleed dark tile as the client's MSIX logo assets.
|
||||||
|
foreach ($pct in 100, 125, 150, 175, 200) {
|
||||||
|
$sz = [int][Math]::Round(55 * $pct / 100.0)
|
||||||
|
$bmp, $g = New-Canvas ($sz * $SS) ($sz * $SS)
|
||||||
|
$b = New-Object System.Drawing.SolidBrush($colTile)
|
||||||
|
$g.FillRectangle($b, 0, 0, ($sz * $SS), ($sz * $SS)); $b.Dispose()
|
||||||
|
Draw-Mark $g ($sz * $SS / 2.0) ($sz * $SS / 2.0) ($sz * $SS * 0.74)
|
||||||
|
$g.Dispose()
|
||||||
|
$out = Resize-Bitmap $bmp $sz $sz
|
||||||
|
$bmp.Dispose()
|
||||||
|
Save-Bmp24 $out (Join-Path $OutDir ("wizard-small-{0}.bmp" -f $pct))
|
||||||
|
$out.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- punktfunk.ico: rounded brand tile at 16..256 --------------------------------------------
|
||||||
|
# Small sizes are classic 32bpp DIB entries (Inno's SetupIconFile resource updater and older shell
|
||||||
|
# consumers reject an all-PNG icon); only 128/256 use PNG compression (the standard Vista+ layout).
|
||||||
|
function New-IconTile([int]$sz) {
|
||||||
|
$bmp, $g = New-Canvas ($sz * $SS) ($sz * $SS)
|
||||||
|
# Rounded-rect tile (22% corner radius - the Windows 11 app-icon look).
|
||||||
|
$S = $sz * $SS; $rad = [int]($S * 0.22)
|
||||||
|
$path = New-Object System.Drawing.Drawing2D.GraphicsPath
|
||||||
|
$path.AddArc(0, 0, 2 * $rad, 2 * $rad, 180, 90)
|
||||||
|
$path.AddArc($S - 2 * $rad, 0, 2 * $rad, 2 * $rad, 270, 90)
|
||||||
|
$path.AddArc($S - 2 * $rad, $S - 2 * $rad, 2 * $rad, 2 * $rad, 0, 90)
|
||||||
|
$path.AddArc(0, $S - 2 * $rad, 2 * $rad, 2 * $rad, 90, 90)
|
||||||
|
$path.CloseFigure()
|
||||||
|
$b = New-Object System.Drawing.SolidBrush($colTile)
|
||||||
|
$g.FillPath($b, $path); $b.Dispose(); $path.Dispose()
|
||||||
|
Draw-Mark $g ($S / 2.0) ($S / 2.0) ($S * 0.74)
|
||||||
|
$g.Dispose()
|
||||||
|
$out = Resize-Bitmap $bmp $sz $sz
|
||||||
|
$bmp.Dispose()
|
||||||
|
$out
|
||||||
|
}
|
||||||
|
|
||||||
|
# PNG-compressed entry payload (used for the 128/256 entries). The leading comma keeps the byte[]
|
||||||
|
# a single pipeline object (PowerShell would otherwise unroll it into individual bytes).
|
||||||
|
function ConvertTo-IconPng([System.Drawing.Bitmap]$tile) {
|
||||||
|
$ms = New-Object System.IO.MemoryStream
|
||||||
|
$tile.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
return , $ms.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Classic ICO DIB entry payload: BITMAPINFOHEADER (height doubled) + bottom-up 32bpp BGRA XOR data
|
||||||
|
# + an all-zero 1bpp AND mask (32bpp icons carry transparency in the alpha channel).
|
||||||
|
function ConvertTo-IconDib([System.Drawing.Bitmap]$tile) {
|
||||||
|
$s = $tile.Width
|
||||||
|
$rect = New-Object System.Drawing.Rectangle(0, 0, $s, $s)
|
||||||
|
$data = $tile.LockBits($rect, [System.Drawing.Imaging.ImageLockMode]::ReadOnly,
|
||||||
|
[System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||||
|
$px = New-Object byte[] ($data.Stride * $s)
|
||||||
|
[System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $px, 0, $px.Length)
|
||||||
|
$tile.UnlockBits($data)
|
||||||
|
$maskStride = [int][Math]::Ceiling($s / 32.0) * 4 # 1bpp rows padded to 32 bits
|
||||||
|
$ms = New-Object System.IO.MemoryStream
|
||||||
|
$w = New-Object System.IO.BinaryWriter($ms)
|
||||||
|
$w.Write([uint32]40); $w.Write([int]$s); $w.Write([int]($s * 2)) # biSize, biWidth, biHeight (XOR+AND)
|
||||||
|
$w.Write([uint16]1); $w.Write([uint16]32); $w.Write([uint32]0) # biPlanes, biBitCount, BI_RGB
|
||||||
|
$w.Write([uint32]($s * $s * 4 + $maskStride * $s)) # biSizeImage
|
||||||
|
$w.Write([int]0); $w.Write([int]0); $w.Write([uint32]0); $w.Write([uint32]0)
|
||||||
|
for ($y = $s - 1; $y -ge 0; $y--) { $w.Write($px, $y * $data.Stride, $s * 4) } # XOR, bottom-up
|
||||||
|
$w.Write((New-Object byte[] ($maskStride * $s))) # AND mask: all opaque
|
||||||
|
$w.Flush()
|
||||||
|
$bytes = $ms.ToArray()
|
||||||
|
$w.Dispose(); $ms.Dispose()
|
||||||
|
return , $bytes # leading comma: emit the byte[] as ONE object, not unrolled bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
$icoSizes = 16, 20, 24, 32, 40, 48, 64, 128, 256
|
||||||
|
$pngs = @(foreach ($s in $icoSizes) {
|
||||||
|
$tile = New-IconTile $s
|
||||||
|
if ($s -ge 128) { ConvertTo-IconPng $tile } else { ConvertTo-IconDib $tile }
|
||||||
|
$tile.Dispose()
|
||||||
|
})
|
||||||
|
$ico = New-Object System.IO.MemoryStream
|
||||||
|
$bw = New-Object System.IO.BinaryWriter($ico)
|
||||||
|
# ICONDIR
|
||||||
|
$bw.Write([uint16]0); $bw.Write([uint16]1); $bw.Write([uint16]$icoSizes.Count)
|
||||||
|
# ICONDIRENTRYs (width/height byte 0 means 256)
|
||||||
|
$offset = 6 + 16 * $icoSizes.Count
|
||||||
|
for ($i = 0; $i -lt $icoSizes.Count; $i++) {
|
||||||
|
$s = $icoSizes[$i]
|
||||||
|
$dim = if ($s -ge 256) { 0 } else { $s }
|
||||||
|
$bw.Write([byte]$dim); $bw.Write([byte]$dim) # width, height
|
||||||
|
$bw.Write([byte]0); $bw.Write([byte]0) # colors, reserved
|
||||||
|
$bw.Write([uint16]1); $bw.Write([uint16]32) # planes, bitcount
|
||||||
|
$bw.Write([uint32]$pngs[$i].Length); $bw.Write([uint32]$offset)
|
||||||
|
$offset += $pngs[$i].Length
|
||||||
|
}
|
||||||
|
foreach ($p in $pngs) { $bw.Write([byte[]]$p) }
|
||||||
|
$bw.Flush()
|
||||||
|
$icoPath = Join-Path $OutDir 'punktfunk.ico'
|
||||||
|
[IO.File]::WriteAllBytes($icoPath, $ico.ToArray())
|
||||||
|
$bw.Dispose(); $ico.Dispose()
|
||||||
|
# Self-check: a malformed container here would only surface later as ISCC "Icon file is invalid".
|
||||||
|
$probe = New-Object System.Drawing.Icon($icoPath)
|
||||||
|
$probe.Dispose()
|
||||||
|
Write-Host " wrote $icoPath ($($icoSizes -join ',')) - verified loadable"
|
||||||
|
Write-Host "==> branding assets generated in $OutDir"
|
||||||
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 340 KiB |
|
After Width: | Height: | Size: 464 KiB |
|
After Width: | Height: | Size: 604 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -132,6 +132,13 @@ $issLocal = Join-Path $OutDir 'punktfunk-host.iss'
|
|||||||
Copy-Item -LiteralPath $hostEnvSrc -Destination $hostEnv -Force
|
Copy-Item -LiteralPath $hostEnvSrc -Destination $hostEnv -Force
|
||||||
Copy-Item -LiteralPath $readmeSrc -Destination $readme -Force
|
Copy-Item -LiteralPath $readmeSrc -Destination $readme -Force
|
||||||
Copy-Item -LiteralPath $iss -Destination $issLocal -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
|
# 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
|
# generated third-party crate notices. The FFmpeg LGPL notice + license text are added to this same
|
||||||
@@ -198,7 +205,13 @@ if (-not $NoDriver) {
|
|||||||
# shipped intact); supply it via -VbCableDir / $env:VBCABLE_DIR pointing at the extracted official
|
# 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
|
# 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.
|
# host then auto-installs the Steam Streaming pair as a fallback and mic passthrough needs a manual cable.
|
||||||
if ($VbCableDir -and (Test-Path $VbCableDir) -and (Get-ChildItem -Path $VbCableDir -Filter 'VBCABLE_Setup*.exe' -ErrorAction SilentlyContinue)) {
|
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'
|
$vbStage = Join-Path $OutDir 'vbcable'
|
||||||
if (Test-Path $vbStage) { Remove-Item -Recurse -Force $vbStage }
|
if (Test-Path $vbStage) { Remove-Item -Recurse -Force $vbStage }
|
||||||
New-Item -ItemType Directory -Force -Path $vbStage | Out-Null
|
New-Item -ItemType Directory -Force -Path $vbStage | Out-Null
|
||||||
@@ -211,7 +224,7 @@ if ($VbCableDir -and (Test-Path $VbCableDir) -and (Get-ChildItem -Path $VbCableD
|
|||||||
Copy-Item (Join-Path $here 'licenses\VB-CABLE-NOTICE.txt') -Destination $licStage -Force
|
Copy-Item (Join-Path $here 'licenses\VB-CABLE-NOTICE.txt') -Destination $licStage -Force
|
||||||
Write-Host "==> bundling VB-CABLE (virtual mic) from $VbCableDir -> $vbStage"
|
Write-Host "==> bundling VB-CABLE (virtual mic) from $VbCableDir -> $vbStage"
|
||||||
}
|
}
|
||||||
else { Write-Host "no -VbCableDir/`$env:VBCABLE_DIR (or no VBCABLE_Setup*.exe in it) -> installer built WITHOUT the bundled VB-CABLE virtual mic" }
|
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) ------------------------------------
|
# --- 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
|
# A host built with --features amf-qsv link-imports avcodec/avutil/swscale/... so the shared DLLs
|
||||||
|
|||||||
@@ -28,6 +28,13 @@
|
|||||||
#ifndef Readme
|
#ifndef Readme
|
||||||
#define Readme "README.md"
|
#define Readme "README.md"
|
||||||
#endif
|
#endif
|
||||||
|
; Branding assets (wizard side panel + header tile BMPs, setup/app icon), generated + committed by
|
||||||
|
; branding/gen-branding.ps1 from the canonical brand-mark geometry. Relative to this script's dir:
|
||||||
|
; works from the repo checkout AND from the staged copy (pack-host-installer.ps1 stages branding\
|
||||||
|
; next to the staged .iss).
|
||||||
|
#ifndef BrandingDir
|
||||||
|
#define BrandingDir "branding"
|
||||||
|
#endif
|
||||||
; The web console launcher (the PunktfunkWeb task action) + its post-install provisioner - committed
|
; The web console launcher (the PunktfunkWeb task action) + its post-install provisioner - committed
|
||||||
; scripts staged next to the .iss by pack-host-installer.ps1 (absolute paths passed in).
|
; scripts staged next to the .iss by pack-host-installer.ps1 (absolute paths passed in).
|
||||||
#ifndef WebRunCmd
|
#ifndef WebRunCmd
|
||||||
@@ -85,9 +92,23 @@ OutputDir={#OutputDir}
|
|||||||
OutputBaseFilename=punktfunk-host-setup-{#MyAppVersion}
|
OutputBaseFilename=punktfunk-host-setup-{#MyAppVersion}
|
||||||
Compression=lzma2/max
|
Compression=lzma2/max
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
|
; Modern branded wizard: Windows-11-style controls that follow the system light/dark theme
|
||||||
|
; (Inno Setup >= 6.6; CI provisions current 6.x via choco). An older local compiler falls back
|
||||||
|
; to the plain modern style so a dev pack still builds.
|
||||||
|
#if VER >= EncodeVer(6,6,0)
|
||||||
|
WizardStyle=modern dynamic windows11
|
||||||
|
#else
|
||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
|
#endif
|
||||||
|
; Brand assets (branding/gen-branding.ps1): the violet lens mark on a dark panel/tile - self-
|
||||||
|
; contained dark art, so it reads correctly in both the light and dark wizard appearance. The
|
||||||
|
; wildcard names carry 100..200% DPI variants; Setup picks the closest.
|
||||||
|
SetupIconFile={#BrandingDir}\punktfunk.ico
|
||||||
|
WizardImageFile={#BrandingDir}\wizard-image-*.bmp
|
||||||
|
WizardSmallImageFile={#BrandingDir}\wizard-small-*.bmp
|
||||||
UninstallDisplayName=punktfunk host {#MyAppVersion}
|
UninstallDisplayName=punktfunk host {#MyAppVersion}
|
||||||
UninstallDisplayIcon={app}\punktfunk-host.exe
|
; The branded multi-size .ico (installed below) - the host exe embeds no icon resource.
|
||||||
|
UninstallDisplayIcon={app}\punktfunk.ico
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
@@ -100,17 +121,26 @@ Name: "installdriver"; Description: "Install the pf-vdisplay virtual display dri
|
|||||||
Name: "installgamepad"; Description: "Install the virtual gamepad drivers (DualSense / DualShock 4 / Xbox 360 - no ViGEmBus needed)"
|
Name: "installgamepad"; Description: "Install the virtual gamepad drivers (DualSense / DualShock 4 / Xbox 360 - no ViGEmBus needed)"
|
||||||
#endif
|
#endif
|
||||||
#ifdef WithAudioCable
|
#ifdef WithAudioCable
|
||||||
Name: "installaudiocable"; Description: "Install VB-CABLE virtual audio (microphone passthrough - VB-Audio donationware, www.vb-cable.com)"
|
; VB-Audio's bundling grant requires the end user to see VB-CABLE's origin + donationware status
|
||||||
|
; at install time - keep the vendor, URL, and donationware wording in this visible task text (the
|
||||||
|
; full notice ships in {app}\licenses\VB-CABLE-NOTICE.txt).
|
||||||
|
Name: "installaudiocable"; Description: "Install VB-CABLE virtual audio for microphone passthrough (VB-CABLE by VB-Audio, www.vb-cable.com - donationware, all participations welcome)"
|
||||||
#endif
|
#endif
|
||||||
#ifdef WithVkLayer
|
#ifdef WithVkLayer
|
||||||
Name: "installhdrlayer"; Description: "Install the HDR Vulkan layer (lets Vulkan games like Doom use HDR on the virtual display)"
|
Name: "installhdrlayer"; Description: "Install the HDR Vulkan layer (lets Vulkan games like Doom use HDR on the virtual display)"
|
||||||
#endif
|
#endif
|
||||||
|
; Host-config choice, applied via `service install --gamestream=on|off` (writes PUNKTFUNK_HOST_CMD
|
||||||
|
; in host.env; a hand-customized value is left alone). Checked = the Moonlight-compatible unified
|
||||||
|
; host (the common Windows setup); unchecked = the secure native-only host (punktfunk clients only).
|
||||||
|
Name: "gamestream"; Description: "Enable GameStream (Moonlight) compatibility - lets stock Moonlight clients connect (uses legacy plain-HTTP pairing; for trusted LANs)"
|
||||||
Name: "startservice"; Description: "Start the punktfunk host service now (also starts on every boot)"
|
Name: "startservice"; Description: "Start the punktfunk host service now (also starts on every boot)"
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "{#BinDir}\punktfunk-host.exe"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "{#BinDir}\punktfunk-host.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "{#HostEnv}"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "{#HostEnv}"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "{#Readme}"; DestDir: "{app}"; DestName: "README.txt"; Flags: ignoreversion
|
Source: "{#Readme}"; DestDir: "{app}"; DestName: "README.txt"; Flags: ignoreversion
|
||||||
|
; The branded icon, referenced by UninstallDisplayIcon (Apps & features shows it for the entry).
|
||||||
|
Source: "{#BrandingDir}\punktfunk.ico"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
#ifdef LicensesDir
|
#ifdef LicensesDir
|
||||||
; License/attribution payload -> {app}\licenses: the project's MIT/Apache texts, the generated
|
; License/attribution payload -> {app}\licenses: the project's MIT/Apache texts, the generated
|
||||||
; THIRD-PARTY-NOTICES (permissive crate attributions), and (on an amf-qsv build) the FFmpeg LGPL
|
; THIRD-PARTY-NOTICES (permissive crate attributions), and (on an amf-qsv build) the FFmpeg LGPL
|
||||||
@@ -184,7 +214,8 @@ Filename: "powershell.exe"; \
|
|||||||
#endif
|
#endif
|
||||||
; Register (or re-point, on upgrade - idempotent) the SYSTEM service from its FINAL {app} location:
|
; 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}.
|
; 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}"; \
|
; --gamestream=on|off carries the wizard's GameStream task choice into host.env's PUNKTFUNK_HOST_CMD.
|
||||||
|
Filename: "{app}\punktfunk-host.exe"; Parameters: "service install {code:GamestreamParam}"; WorkingDir: "{app}"; \
|
||||||
StatusMsg: "Registering the punktfunk host service..."; Flags: runhidden waituntilterminated
|
StatusMsg: "Registering the punktfunk host service..."; Flags: runhidden waituntilterminated
|
||||||
Filename: "{app}\punktfunk-host.exe"; Parameters: "service start"; WorkingDir: "{app}"; \
|
Filename: "{app}\punktfunk-host.exe"; Parameters: "service start"; WorkingDir: "{app}"; \
|
||||||
StatusMsg: "Starting the punktfunk host service..."; Flags: runhidden waituntilterminated; Tasks: startservice
|
StatusMsg: "Starting the punktfunk host service..."; Flags: runhidden waituntilterminated; Tasks: startservice
|
||||||
@@ -198,6 +229,14 @@ Filename: "{app}\punktfunk-host.exe"; Parameters: "web setup {code:WebSetupParam
|
|||||||
|
|
||||||
[UninstallRun]
|
[UninstallRun]
|
||||||
Filename: "{app}\punktfunk-host.exe"; Parameters: "service uninstall"; Flags: runhidden waituntilterminated; RunOnceId: "PunktfunkHostServiceUninstall"
|
Filename: "{app}\punktfunk-host.exe"; Parameters: "service uninstall"; Flags: runhidden waituntilterminated; RunOnceId: "PunktfunkHostServiceUninstall"
|
||||||
|
; Remove the punktfunk drivers we installed (pf-vdisplay devnode + driver package, then the gamepad
|
||||||
|
; driver packages). AFTER service uninstall so the host no longer holds the devices. Unconditional
|
||||||
|
; (not #ifdef'd on this build's bundled payload - an upgrade may have dropped a payload the original
|
||||||
|
; install laid down); `driver uninstall` is best-effort and no-ops when nothing is installed.
|
||||||
|
; VB-CABLE is deliberately NOT removed: it is a third-party shared component the user may use
|
||||||
|
; elsewhere - see licenses\VB-CABLE-NOTICE.txt for its own uninstall.
|
||||||
|
Filename: "{app}\punktfunk-host.exe"; Parameters: "driver uninstall"; Flags: runhidden waituntilterminated; RunOnceId: "PunktfunkVdisplayDriverUninstall"
|
||||||
|
Filename: "{app}\punktfunk-host.exe"; Parameters: "driver uninstall --gamepad"; Flags: runhidden waituntilterminated; RunOnceId: "PunktfunkGamepadDriverUninstall"
|
||||||
#ifdef WithWeb
|
#ifdef WithWeb
|
||||||
; Stop + remove the PunktfunkWeb task and its firewall rule (leaves %ProgramData%\punktfunk config,
|
; Stop + remove the PunktfunkWeb task and its firewall rule (leaves %ProgramData%\punktfunk config,
|
||||||
; like the host uninstall does).
|
; like the host uninstall does).
|
||||||
@@ -207,6 +246,17 @@ Filename: "powershell.exe"; \
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
|
{ The GameStream task choice, forwarded to `service install` (which writes host.env's
|
||||||
|
PUNKTFUNK_HOST_CMD - only if it is unset or still one of the two canonical values, so a
|
||||||
|
hand-customized command line survives upgrades). }
|
||||||
|
function GamestreamParam(Param: String): String;
|
||||||
|
begin
|
||||||
|
if WizardIsTaskSelected('gamestream') then
|
||||||
|
Result := '--gamestream=on'
|
||||||
|
else
|
||||||
|
Result := '--gamestream=off';
|
||||||
|
end;
|
||||||
|
|
||||||
#ifdef WithWeb
|
#ifdef WithWeb
|
||||||
var
|
var
|
||||||
WebPwPage: TInputQueryWizardPage;
|
WebPwPage: TInputQueryWizardPage;
|
||||||
|
|||||||
@@ -52,22 +52,48 @@ Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg" -ZipTag 'win64'
|
|||||||
Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg-arm64" -ZipTag 'winarm64'
|
Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg-arm64" -ZipTag 'winarm64'
|
||||||
|
|
||||||
# --- Inno Setup (ISCC.exe) for the host installer build (windows-host.yml). pack-host-installer.ps1
|
# --- Inno Setup (ISCC.exe) for the host installer build (windows-host.yml). pack-host-installer.ps1
|
||||||
# locates it at its fixed Program Files path, so it need not be on PATH - just present. ---
|
# locates it at its fixed Program Files path, so it need not be on PATH - just present. The .iss
|
||||||
if (-not (Test-Path "C:\Program Files (x86)\Inno Setup 6\ISCC.exe")) {
|
# uses the 6.6+ styling (WizardStyle dark/dynamic + the windows11 style); an older 6.x compiles a
|
||||||
|
# plain-modern fallback, so upgrade a pre-6.6 install rather than silently shipping the old look. ---
|
||||||
|
$isccPath = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
|
||||||
|
$innoVer = (Get-ItemProperty 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1' -ErrorAction SilentlyContinue).DisplayVersion
|
||||||
|
if (-not (Test-Path $isccPath) -or ($innoVer -and [version]$innoVer -lt [version]'6.6.0')) {
|
||||||
if (Get-Command choco -ErrorAction SilentlyContinue) {
|
if (Get-Command choco -ErrorAction SilentlyContinue) {
|
||||||
info "installing Inno Setup (ISCC)"
|
info "installing/upgrading Inno Setup (ISCC; found: $innoVer)"
|
||||||
choco install innosetup -y --no-progress
|
choco upgrade innosetup -y --no-progress
|
||||||
} else { Write-Warning "Inno Setup not found and choco unavailable - install it for windows-host.yml." }
|
} else { Write-Warning "Inno Setup missing or pre-6.6 ($innoVer) and choco unavailable - install/upgrade it for windows-host.yml." }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- VB-CABLE (the streaming virtual microphone the host installer bundles). Pinned official
|
||||||
|
# package, SHA-256 verified - a silent hash change means VB-Audio shipped a new pack: verify it,
|
||||||
|
# then update BOTH the pin here and the notice if terms changed (packaging/windows/licenses/
|
||||||
|
# VB-CABLE-NOTICE.txt). Donationware by VB-Audio (https://vb-audio.com), redistributed under
|
||||||
|
# VB-Audio's bundling grant; only the base cable, never A+B/C+D. windows-host.yml points
|
||||||
|
# VBCABLE_DIR here so pack-host-installer.ps1 bundles it. ---
|
||||||
|
$vbDir = "C:\Users\Public\vbcable"
|
||||||
|
$vbUrl = "https://download.vb-audio.com/Download_CABLE/VBCABLE_Driver_Pack45.zip"
|
||||||
|
$vbSha = "B950E39F01AF1D04EA623C8F6D8EB9B6EA5C477C637295FABF20631C85116BFB"
|
||||||
|
if (-not (Test-Path (Join-Path $vbDir 'VBCABLE_Setup_x64.exe'))) {
|
||||||
|
info "fetching VB-CABLE (official base package, pinned)"
|
||||||
|
$vbZip = "$vbDir.zip"
|
||||||
|
Invoke-WebRequest -Uri $vbUrl -OutFile $vbZip -UseBasicParsing
|
||||||
|
$got = (Get-FileHash $vbZip -Algorithm SHA256).Hash
|
||||||
|
if ($got -ne $vbSha) { Remove-Item $vbZip -Force; throw "VB-CABLE download hash mismatch (got $got, pinned $vbSha) - vendor package changed; re-verify before re-pinning." }
|
||||||
|
if (Test-Path $vbDir) { Remove-Item -Recurse -Force $vbDir }
|
||||||
|
Expand-Archive -Path $vbZip -DestinationPath $vbDir -Force # flat zip (setup exes + signed drivers)
|
||||||
|
Remove-Item $vbZip -Force
|
||||||
|
info "VB-CABLE staged at $vbDir"
|
||||||
|
} else { info "VB-CABLE already present at $vbDir" }
|
||||||
|
|
||||||
# --- Drop punktfunk's env vars into the generic runner's daemon wrapper extension point (see
|
# --- Drop punktfunk's env vars into the generic runner's daemon wrapper extension point (see
|
||||||
# unom/infra's scripts/setup-gitea-runner-base.ps1) so the act_runner daemon - and therefore every
|
# unom/infra's scripts/setup-gitea-runner-base.ps1) so the act_runner daemon - and therefore every
|
||||||
# job it runs - sees FFMPEG_DIR without unom/infra needing to know punktfunk exists. ---
|
# job it runs - sees FFMPEG_DIR without unom/infra needing to know punktfunk exists. ---
|
||||||
$projectEnv = "C:\Users\Public\act-runner\project-env.ps1"
|
$projectEnv = "C:\Users\Public\act-runner\project-env.ps1"
|
||||||
@'
|
@'
|
||||||
$env:FFMPEG_DIR = "C:\Users\Public\ffmpeg"
|
$env:FFMPEG_DIR = "C:\Users\Public\ffmpeg"
|
||||||
|
$env:VBCABLE_DIR = "C:\Users\Public\vbcable"
|
||||||
$env:PATH = "C:\Users\Public\ffmpeg\bin;" + $env:PATH
|
$env:PATH = "C:\Users\Public\ffmpeg\bin;" + $env:PATH
|
||||||
'@ | Set-Content -Encoding UTF8 $projectEnv
|
'@ | Set-Content -Encoding UTF8 $projectEnv
|
||||||
info "wrote $projectEnv (FFMPEG_DIR) - restart the gitea-act-runner scheduled task to pick it up"
|
info "wrote $projectEnv (FFMPEG_DIR, VBCABLE_DIR) - restart the gitea-act-runner scheduled task to pick it up"
|
||||||
|
|
||||||
info "punktfunk extras provisioned OK."
|
info "punktfunk extras provisioned OK."
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ RUST_LOG=info
|
|||||||
|
|
||||||
# The host subcommand the service launches. Default: `serve --gamestream` (native punktfunk/1 host
|
# The host subcommand the service launches. Default: `serve --gamestream` (native punktfunk/1 host
|
||||||
# ALWAYS on + the GameStream/Moonlight-compat planes). Use `serve` for a SECURE native-only host
|
# ALWAYS on + the GameStream/Moonlight-compat planes). Use `serve` for a SECURE native-only host
|
||||||
# (no plain-HTTP pairing / legacy GCM nonce reuse — security-review #5/#9). Uncomment to override.
|
# (no plain-HTTP pairing / legacy GCM nonce reuse — security-review #5/#9). The installer's
|
||||||
|
# "Enable GameStream (Moonlight) compatibility" task sets this; a custom value you write here is
|
||||||
|
# never overwritten by a reinstall/upgrade.
|
||||||
#PUNKTFUNK_HOST_CMD=serve --gamestream
|
#PUNKTFUNK_HOST_CMD=serve --gamestream
|
||||||
|
|
||||||
# Multi-GPU boxes only: force the NVENC/Desktop-Duplication GPU by Description substring. Leave
|
# Multi-GPU boxes only: force the NVENC/Desktop-Duplication GPU by Description substring. Leave
|
||||||
|
|||||||