From e9e1ec7dc50000072d3ecaac1c4caa27d53e9894 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 8 Aug 2026 01:43:11 +0200 Subject: [PATCH 1/6] fix(pf-inject): the DualShock 4 Windows backend never imported OFF_INPUT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Windows host does not build: error[E0425]: cannot find value `OFF_INPUT` in this scope --> crates\pf-inject\src\inject\windows\dualshock4_windows.rs:65:48 error: could not compile `pf-inject` (lib) due to 1 previous error `dualshock4_windows.rs` writes the neutral report straight to `OFF_INPUT` in its bootstrap path — correctly, and exactly as the DualSense and Steam Deck backends do: the devnode does not exist yet at that point, so there is no reader to race and no seqlock to take. Its steady-state path already goes through `publish_input`, which is the v2.3 seqlock. But the import list only names `publish_input`. `steam_deck_windows.rs` imports `OFF_INPUT` explicitly for the same bootstrap write; this one was missed when the list was edited to add `publish_input`. One word in a `use`. No behaviour. WHY CI DID NOT CATCH IT: `pf-inject`'s Windows backends compile only for `*-pc-windows-msvc`, and the crate is host-side, so the client Windows workflow never touches it. A cargo check from a Mac cannot stand in either — pf-inject pulls punktfunk-core and therefore ring, whose C build wants MSVC headers, so the cross-check dies in cc-rs long before it reaches this file. FOUND BY: running windows-host.yml's own build line on the CI runner (.133) against the v0.25.0 release tree before tagging — `cargo build --release -p punktfunk-host --features nvenc,amf-qsv,qsv`. It fails at `pf-inject`, which is step 1 of the host job, so a v0.25.0 tag would have produced no Windows host binary, no installer, and no host asset on the release. --- crates/pf-inject/src/inject/windows/dualshock4_windows.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pf-inject/src/inject/windows/dualshock4_windows.rs b/crates/pf-inject/src/inject/windows/dualshock4_windows.rs index 2d839314..1418291a 100644 --- a/crates/pf-inject/src/inject/windows/dualshock4_windows.rs +++ b/crates/pf-inject/src/inject/windows/dualshock4_windows.rs @@ -10,7 +10,7 @@ use super::dualsense_proto::DsState; use super::dualsense_windows::{ create_swdevice, publish_input, OutputDrain, SwDeviceProfile, DEVTYPE_DUALSHOCK4, OFF_DEVTYPE, - OFF_DRIVER_PROTO, OFF_OUT_RING_VER, OFF_PAD_INDEX, SHM_MAGIC, SHM_SIZE, + OFF_DRIVER_PROTO, OFF_INPUT, OFF_OUT_RING_VER, OFF_PAD_INDEX, SHM_MAGIC, SHM_SIZE, }; use super::dualshock4_proto::{ parse_ds4_output, serialize_state, Ds4Feedback, DS4_INPUT_REPORT_LEN, DS4_TOUCH_H, DS4_TOUCH_W, From be86cfcdc0e5add8f7b88b1175c228f7c8396c26 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 8 Aug 2026 02:04:15 +0200 Subject: [PATCH 2/6] fix(client/audio): the PipeWire callback stops filling the buffer ceiling every cycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The playback process callback sized its writes from the mapped buffer's capacity — PipeWire's quantum-limit, 8192 frames ≈ 170 ms — instead of the graph's per-cycle ask (pw_buffer.requested). Every cycle therefore queued up to 170 ms of PCM downstream of the ring, and, worse, taught JitterPolicy that the device drains 170 ms per callback: the underrun floor (want + one frame) rose above any depth the A/V sync loop may request, so sync measured audio ~280 ms late and was forbidden — by its own continuity rule — from draining it. The first on-glass run of the latency overhaul showed exactly that: audio buffer 272 ms, a/v +284 ms, stable. Honor requested (capacity remains both the ceiling and the fallback for requested == 0), and log requested-vs-capacity once per stream in the shape of the host's per-capture-open quantum line, so the next on-glass report can say which one is sizing the writes. Needs libpipewire >= 0.3.49 (2022-03) for the requested field; every ship target clears that. Verified on .21: cargo clippy -p pf-client-core --all-targets -D warnings clean, 167 tests pass, fmt clean. --- crates/pf-client-core/Cargo.toml | 6 +++++- crates/pf-client-core/src/audio.rs | 29 ++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/pf-client-core/Cargo.toml b/crates/pf-client-core/Cargo.toml index d44fe6b3..ed050f8d 100644 --- a/crates/pf-client-core/Cargo.toml +++ b/crates/pf-client-core/Cargo.toml @@ -118,7 +118,11 @@ rand = "0.9" # need the hidapi driver). Linux links the system SDL3; Windows builds it from source # (no system SDL3 there — same choice as clients/windows). [target.'cfg(target_os = "linux")'.dependencies] -pipewire = "0.9" +# `v0_3_49` for `Buffer::requested` — the graph's per-cycle frame ask, without which the +# playback callback can only size writes from the buffer CEILING (quantum-limit, ~170 ms). +# Pure cfg gate; needs libpipewire ≥ 0.3.49 (2022-03) at runtime, which every ship target +# (SteamOS, flatpak runtimes, Arch, Ubuntu ≥ 22.10) clears. +pipewire = { version = "0.9", features = ["v0_3_49"] } sdl3 = { version = "0.18", features = ["hidapi"] } # Native VAAPI decode (M6 of the native-decode program): the hand-declared libva buffer # layouts, the profile/format/surface decisions, the AuPlan → picparams/IQ/slice diff --git a/crates/pf-client-core/src/audio.rs b/crates/pf-client-core/src/audio.rs index 59636a35..31e765a7 100644 --- a/crates/pf-client-core/src/audio.rs +++ b/crates/pf-client-core/src/audio.rs @@ -274,14 +274,41 @@ fn pw_thread( chunk.clear(); let _ = ud.recycle.try_send(chunk); } + // The graph asks for `requested` frames this cycle (one quantum, after + // rate-matching); the mapped buffer is sized for the WORST case — PipeWire's + // `quantum-limit`, 8192 frames ≈ 170 ms — not for this cycle. Filling to + // capacity queued ~170 ms per buffer downstream of the ring and, worse, taught + // the jitter policy that the device drains 170 ms per callback, which lifted + // the underrun floor (`want` + one frame) above any depth the A/V sync loop is + // allowed to ask for: audio sat a stable ~270 ms late and, by the continuity + // rule, sync was FORBIDDEN from draining it. Capacity is only the ceiling; + // `requested == 0` (no adapter suggestion) falls back to it. + let requested = usize::try_from(buffer.requested()).unwrap_or(0); let stride = 4 * ud.channels; // F32LE interleaved let datas = buffer.datas_mut(); if datas.is_empty() { return; } let data = &mut datas[0]; - let want_frames = data.data().map(|s| s.len() / stride).unwrap_or(0); + let max_frames = data.data().map(|s| s.len() / stride).unwrap_or(0); + let want_frames = if requested > 0 { + requested.min(max_frames) + } else { + max_frames + }; let want = want_frames * ud.channels; + // Once per stream, in the shape of the host's per-capture-open quantum log: + // whether the graph's request or the buffer ceiling is sizing our writes is + // exactly what an on-glass latency report needs to say. + if ud.callbacks == 0 { + tracing::info!( + requested_frames = requested, + capacity_frames = max_frames, + write_frames = want_frames, + write_ms = want_frames / 48, + "audio playback quantum" + ); + } // A/V sync: take whatever depth the decode thread's sync loop last asked for, and // publish where the ring actually is so it can measure the result. The policy From 10a0ef3283a59752f2e3560c07bad11d2d8461cd Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 8 Aug 2026 02:19:06 +0200 Subject: [PATCH 3/6] style(plugin-kit): adopt the biome config its own plugins already use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kit had NO biome config and no lint script, while every plugin repo that consumes it has both. So its source quietly drifted — unused imports, unsorted imports, formatting — with nothing to catch any of it. Running biome here for the first time reported 20 findings across 8 files. Adds `plugin-kit/biome.json` mirroring the plugin repos' (tab indent, double quotes, recommended lint preset, organizeImports), a `check` script, and `@biomejs/biome` pinned to the same `^2.5.2` the plugins pin — without that pin `bunx biome` resolved 2.4.6, which rejects the 2.5 `rules.preset` key. Two deliberate differences from the plugin repos' copy: * no `vcs.useIgnoreFile` — those are standalone repos with a .gitignore beside the config; plugin-kit is a directory inside this one, and biome errors with "couldn't find an ignore file". The `files.includes` exclusions cover it. * `!examples/**/dist` instead of `!ui/dist` — the kit has examples, not a UI. `css.parser.tailwindDirectives` is carried over and is load-bearing: without it biome cannot parse `@theme` in src/theme.css and reports three parse errors on CSS that is perfectly valid Tailwind v4. Everything here is formatter/import churn except two real findings, both fixed: * `Layer` (library/define.ts) and `Cause` (sync-engine.ts) were imported and never used; * test/spike-httpapi.test.ts read `(reg?.body as …).ui.secret` one line after `expect(reg).toBeDefined()`. The optional chain undoes the assertion: had `reg` been undefined the `.ui` access would throw a TypeError instead of failing the test readably. Now asserted to the type system too. Wired into plugin-kit-publish.yml as a `Lint & format` step ahead of Typecheck, so this cannot rot again. Gates after: biome clean (42 files), tsc clean, 67/67 tests, build clean. --- .gitea/workflows/plugin-kit-publish.yml | 7 +++ plugin-kit/biome.json | 46 ++++++++++++++++++ plugin-kit/bun.lock | 21 +++++++++ plugin-kit/examples/lutris-plugin.ts | 7 +-- plugin-kit/package.json | 16 +++++-- plugin-kit/src/cache-store.ts | 12 ++--- plugin-kit/src/cli.ts | 10 ++-- plugin-kit/src/config.ts | 11 ++--- plugin-kit/src/host-client.ts | 18 +++---- plugin-kit/src/index.ts | 29 ++++++------ plugin-kit/src/library/define.ts | 26 +++++++---- plugin-kit/src/library/parsers/art.ts | 12 ++++- plugin-kit/src/library/parsers/fs.ts | 2 +- plugin-kit/src/library/parsers/http.ts | 3 +- plugin-kit/src/library/parsers/shortcuts.ts | 7 ++- plugin-kit/src/logging.ts | 6 ++- plugin-kit/src/react/index.tsx | 7 ++- plugin-kit/src/reconcile.ts | 4 +- plugin-kit/src/runtime.ts | 4 +- plugin-kit/src/sse.ts | 2 +- plugin-kit/src/sync-engine.ts | 11 +++-- plugin-kit/src/ui-server.ts | 10 ++-- plugin-kit/test/config.test.ts | 5 +- plugin-kit/test/library-config.test.ts | 40 +++++++++++----- plugin-kit/test/library-parity.test.ts | 3 +- plugin-kit/test/spike-client-prefix.test.ts | 8 +--- plugin-kit/test/spike-httpapi.test.ts | 24 ++++++---- plugin-kit/test/sse-live.test.ts | 8 +++- plugin-kit/test/sse.test.ts | 8 +--- plugin-kit/test/sync-engine.test.ts | 52 ++++++++++----------- 30 files changed, 263 insertions(+), 156 deletions(-) create mode 100644 plugin-kit/biome.json diff --git a/.gitea/workflows/plugin-kit-publish.yml b/.gitea/workflows/plugin-kit-publish.yml index d06f8b45..c03665f3 100644 --- a/.gitea/workflows/plugin-kit-publish.yml +++ b/.gitea/workflows/plugin-kit-publish.yml @@ -66,6 +66,13 @@ jobs: test -f node_modules/@punktfunk/host/package.json test -f node_modules/@punktfunk/host/dist/index.d.ts + # The kit had no biome config and no lint step, while every plugin repo that consumes it does + # — so its source drifted (unused imports, formatting) with nothing to catch it. Now gated + # here, on the same config and pinned biome version the plugins use. + - name: Lint & format + working-directory: plugin-kit + run: bun run check + - name: Typecheck working-directory: plugin-kit run: bun run typecheck diff --git a/plugin-kit/biome.json b/plugin-kit/biome.json new file mode 100644 index 00000000..7fe93e0f --- /dev/null +++ b/plugin-kit/biome.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.7/schema.json", + "files": { + "ignoreUnknown": false, + "includes": ["**", "!dist", "!examples/**/dist", "!**/node_modules"] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "css": { + "parser": { + "tailwindDirectives": true + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended", + "suspicious": { + "noArrayIndexKey": "off" + }, + "style": { + "noNonNullAssertion": "off" + }, + "a11y": { + "noLabelWithoutControl": "off" + }, + "correctness": { + "useExhaustiveDependencies": "warn" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/plugin-kit/bun.lock b/plugin-kit/bun.lock index c77a847b..b97b35d0 100644 --- a/plugin-kit/bun.lock +++ b/plugin-kit/bun.lock @@ -5,6 +5,7 @@ "": { "name": "@punktfunk/plugin-kit", "devDependencies": { + "@biomejs/biome": "^2.5.2", "@punktfunk/host": "file:../sdk", "@types/bun": "^1.3.0", "@types/react": "^19.2.16", @@ -25,6 +26,24 @@ "undici": "^8.9.0", }, "packages": { + "@biomejs/biome": ["@biomejs/biome@2.5.7", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.7", "@biomejs/cli-darwin-x64": "2.5.7", "@biomejs/cli-linux-arm64": "2.5.7", "@biomejs/cli-linux-arm64-musl": "2.5.7", "@biomejs/cli-linux-x64": "2.5.7", "@biomejs/cli-linux-x64-musl": "2.5.7", "@biomejs/cli-win32-arm64": "2.5.7", "@biomejs/cli-win32-x64": "2.5.7" }, "bin": { "biome": "bin/biome" } }, "sha512-zr8K/DcY5tYsQOQwqMJ0AWElo6QgmgNI7idXgXLhevVszlt8RGVpesEJPqx3ThazLaOwjJ5Y8fz3BtH5fGZNsw=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vxo/Ls3/PYdQWyLhYYcgMOCzQypAjcY+iihS8M0wW03l16TCLW4zqZzGo75gm1VdCMj38hTVZ31KBWrZ4G9dJw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Cd3Ga61amT/Yl/0x8elP5hhGYaFy4bw6WuysTgf7oo8TA5tJ5A1k+DkVoJ2BHbTVil51gTX9VPzArnrlLJ3Kyg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-rR2QE0yF2GYSuYuKIa7pKvODGJqnOH+2eDREAM8wV+mWKSkMQKdAp4zXEZfTaxY8PMoNONnpgSWcBCyLDPDOKg=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-xPI5yB6XlpDbNkS+bm1t42olw5c4l3UrlOmLg7KtLJvjvkNF/1V4tnUgfkylGIeb3u/T+BzMGYqgQhzjAoJzuQ=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-FQgqJhscrqJUFptGaRSUJWlXAExwWcDwLuK49dvKfkQ1bB5SEEyFssnsxQY83Xm6jR0EbbX3+8+D5bfvYqUG2Q=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-rE5VZi+qtmPgQH+l7jVxYoZ18b/TiHEhulhMpjmCZH1PltSbjRcxNWywC3HZ9tYottG7ORkeTtoscBilKSBm0g=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-Oq4x0CCwP4jirrcTywXs5kOGZ4v5vuEP+gWrbtjApOA2CL9F3F9GlIdQIci8AKSCa/zURanMRpX/4wQ7Am6hHg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.7", "", { "os": "win32", "cpu": "x64" }, "sha512-V+0wu/nrj2S+MhP4EQ0uHNolP0IALEsz45pg0WoKkHfDeh0+ItHwP/p7bX5RPoMOl9NkpHYWdYPhIcy2mACHvQ=="], + "@effect/openapi-generator": ["@effect/openapi-generator@4.0.0-beta.98", "", { "dependencies": { "swagger2openapi": "^7.0.8" }, "peerDependencies": { "@effect/platform-node": "^4.0.0-beta.98", "effect": "^4.0.0-beta.98" }, "bin": { "openapigen": "dist/bin.js" } }, "sha512-7bqawr/HqJWqQ8H/bHyzBlLPA3LIIm3Y+cGYlIxnC/QVK795QpiEXb7uxTnP7V7w49V0sBtTerv4/9ZjsMffLQ=="], "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.98", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.98", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98", "ioredis": "^5.7.0" } }, "sha512-IQu1TiLXQEDSGkDBllyYjVadf+UqdjptryqX4mmktVTTbGDq7X4uVxe7cSgXuqZvyfG6kagTzwj2lfynxOaKQg=="], @@ -49,6 +68,8 @@ "@punktfunk/host": ["@punktfunk/host@file:../sdk", { "devDependencies": { "@effect/openapi-generator": "4.0.0-beta.98", "@effect/platform-node": "4.0.0-beta.98", "@types/bun": "^1.3.0", "bun2nix": "2.1.2", "effect": "^4.0.0-beta.98", "typescript": "^5.9.3" }, "optionalDependencies": { "undici": "^7.0.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" }, "bin": { "punktfunk-scripting": "./dist/runner-cli.js" } }], + "@punktfunk/host": ["@punktfunk/host@file:../sdk", { "devDependencies": { "@effect/openapi-generator": "4.0.0-beta.98", "@effect/platform-node": "4.0.0-beta.98", "@types/bun": "^1.3.0", "bun2nix": "2.1.2", "effect": "^4.0.0-beta.98", "typescript": "^5.9.3" }, "optionalDependencies": { "undici": "^7.0.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" }, "bin": { "punktfunk-scripting": "./dist/runner-cli.js" } }], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], diff --git a/plugin-kit/examples/lutris-plugin.ts b/plugin-kit/examples/lutris-plugin.ts index 1752923b..55c83f10 100644 --- a/plugin-kit/examples/lutris-plugin.ts +++ b/plugin-kit/examples/lutris-plugin.ts @@ -55,9 +55,10 @@ const databaseCandidates = (): string[] => { }; const findDatabase = (cfg: { databasePath?: string }): string | undefined => - [...(cfg.databasePath ? [cfg.databasePath] : []), ...databaseCandidates()].find( - isFile, - ); + [ + ...(cfg.databasePath ? [cfg.databasePath] : []), + ...databaseCandidates(), + ].find(isFile); /** * `/.jpg` across the current, legacy-cache and Flatpak Lutris roots. diff --git a/plugin-kit/package.json b/plugin-kit/package.json index c4cf6fad..4906d1f5 100644 --- a/plugin-kit/package.json +++ b/plugin-kit/package.json @@ -13,7 +13,12 @@ "bugs": { "url": "https://git.unom.io/unom/punktfunk/issues" }, - "keywords": ["punktfunk", "plugin", "framework", "effect"], + "keywords": [ + "punktfunk", + "plugin", + "framework", + "effect" + ], "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { @@ -35,7 +40,10 @@ }, "./theme.css": "./dist/theme.css" }, - "files": ["dist", "README.md"], + "files": [ + "dist", + "README.md" + ], "publishConfig": { "registry": "https://git.unom.io/api/packages/unom/npm/" }, @@ -43,7 +51,8 @@ "typecheck": "tsc --noEmit", "build": "tsc -p tsconfig.build.json && cp src/theme.css dist/theme.css", "test": "bun test", - "prepublishOnly": "bun run build" + "prepublishOnly": "bun run build", + "check": "biome check ." }, "peerDependencies": { "effect": "^4.0.0-beta.98", @@ -56,6 +65,7 @@ } }, "devDependencies": { + "@biomejs/biome": "^2.5.2", "@punktfunk/host": "file:../sdk", "@types/bun": "^1.3.0", "@types/react": "^19.2.16", diff --git a/plugin-kit/src/cache-store.ts b/plugin-kit/src/cache-store.ts index 3be9d4ea..e16a222c 100644 --- a/plugin-kit/src/cache-store.ts +++ b/plugin-kit/src/cache-store.ts @@ -4,8 +4,8 @@ import * as fs from "node:fs"; import { Effect, Ref, Schema } from "effect"; import type { ConfigWriteError } from "./errors.js"; -import { atomicWriteFile, ensureStateDir, statePath } from "./paths.js"; import { PluginInfo } from "./host-client.js"; +import { atomicWriteFile, ensureStateDir, statePath } from "./paths.js"; export interface CacheStore { readonly get: Effect.Effect; @@ -44,20 +44,14 @@ export const makeCacheStore = (opts: { const persist = (value: S["Type"]) => ensureStateDir(info.name).pipe( - Effect.flatMap(() => - atomicWriteFile(file, JSON.stringify(value)), - ), + Effect.flatMap(() => atomicWriteFile(file, JSON.stringify(value))), ); const modify = (f: (current: S["Type"]) => readonly [A, S["Type"]]) => Ref.modify(ref, (current) => { const [a, next] = f(current); return [[a, next] as const, next] as const; - }).pipe( - Effect.flatMap(([a, next]) => - persist(next).pipe(Effect.as(a)), - ), - ); + }).pipe(Effect.flatMap(([a, next]) => persist(next).pipe(Effect.as(a)))); return { get: Ref.get(ref), diff --git a/plugin-kit/src/cli.ts b/plugin-kit/src/cli.ts index cb0dabab..e7315829 100644 --- a/plugin-kit/src/cli.ts +++ b/plugin-kit/src/cli.ts @@ -5,13 +5,13 @@ // ManagedRuntime + layer graph as the plugin entry, so commands reuse the exact services. import { connect, type Punktfunk } from "@punktfunk/host"; import { Effect, Layer, ManagedRuntime } from "effect"; +import { HostRequestError } from "./errors.js"; import { type HostClient, hostClientFromFacade, type PluginInfo, pluginInfoLayer, } from "./host-client.js"; -import { HostRequestError } from "./errors.js"; import { loggingLayer } from "./logging.js"; import type { PluginKitDef } from "./runtime.js"; @@ -62,9 +62,7 @@ export const runPluginCli = async (opts: { process.exit(name === undefined || name === "help" ? 0 : 2); } - const pf = command.offline - ? offlineFacade(opts.def.name) - : await connect(); + const pf = command.offline ? offlineFacade(opts.def.name) : await connect(); const base = Layer.mergeAll( hostClientFromFacade(pf), pluginInfoLayer({ name: opts.def.name, version: opts.def.version }), @@ -82,9 +80,7 @@ export const runPluginCli = async (opts: { process.exitCode ??= 0; } catch (e) { const hint = - e instanceof HostRequestError - ? " (is the Punktfunk host running?)" - : ""; + e instanceof HostRequestError ? " (is the Punktfunk host running?)" : ""; console.error(`${opts.def.name}: ${name} failed: ${e}${hint}`); process.exitCode = 1; } finally { diff --git a/plugin-kit/src/config.ts b/plugin-kit/src/config.ts index 9c9411a3..e1b8e9ca 100644 --- a/plugin-kit/src/config.ts +++ b/plugin-kit/src/config.ts @@ -16,8 +16,8 @@ import { ConfigPermissionError, type ConfigWriteError, } from "./errors.js"; -import { atomicWriteFile, ensureStateDir, statePath } from "./paths.js"; import { PluginInfo } from "./host-client.js"; +import { atomicWriteFile, ensureStateDir, statePath } from "./paths.js"; export interface ConfigService { /** Decode the raw file with Schema defaults applied. Missing file → all defaults. */ @@ -36,10 +36,7 @@ export interface ConfigService { */ readonly saveRaw: ( raw: unknown, - ) => Effect.Effect< - S["Type"], - ConfigParseError | ConfigWriteError - >; + ) => Effect.Effect; /** Emits the decoded config after every successful `saveRaw`. */ readonly changes: Stream.Stream; /** Absolute path of the config file (status views). */ @@ -95,9 +92,7 @@ export const makeConfigService = (opts: { const file = statePath(info.name, opts.fileName ?? "config.json"); const hub = yield* PubSub.unbounded(); - const decode = ( - raw: unknown, - ): Effect.Effect => + const decode = (raw: unknown): Effect.Effect => Schema.decodeUnknownEffect(opts.schema)(raw).pipe( Effect.mapError( (e) => new ConfigParseError({ path: file, issue: String(e) }), diff --git a/plugin-kit/src/host-client.ts b/plugin-kit/src/host-client.ts index ea3caf5a..a127449e 100644 --- a/plugin-kit/src/host-client.ts +++ b/plugin-kit/src/host-client.ts @@ -21,14 +21,13 @@ export interface HostClientService { readonly facade: Punktfunk; } -export class HostClient extends Context.Service()( - "@punktfunk/plugin-kit/HostClient", -) {} +export class HostClient extends Context.Service< + HostClient, + HostClientService +>()("@punktfunk/plugin-kit/HostClient") {} /** Wrap the facade the runner hands to `main` (or `connect()` in the CLI/dev paths). */ -export const hostClientFromFacade = ( - pf: Punktfunk, -): Layer.Layer => +export const hostClientFromFacade = (pf: Punktfunk): Layer.Layer => Layer.succeed(HostClient)({ request: (method, path, body) => Effect.tryPromise({ @@ -44,9 +43,10 @@ export interface PluginInfoService { readonly version?: string; } -export class PluginInfo extends Context.Service()( - "@punktfunk/plugin-kit/PluginInfo", -) {} +export class PluginInfo extends Context.Service< + PluginInfo, + PluginInfoService +>()("@punktfunk/plugin-kit/PluginInfo") {} export const pluginInfoLayer = ( info: PluginInfoService, diff --git a/plugin-kit/src/index.ts b/plugin-kit/src/index.ts index cd4aef44..d80b2bb2 100644 --- a/plugin-kit/src/index.ts +++ b/plugin-kit/src/index.ts @@ -1,5 +1,18 @@ // @punktfunk/plugin-kit — Effect-based framework for punktfunk plugins. + +export { type CacheStore, makeCacheStore } from "./cache-store.js"; +export { type CliCommand, runPluginCli } from "./cli.js"; +export { type ConfigService, makeConfigService } from "./config.js"; export * from "./errors.js"; +export { + HostClient, + type HostClientService, + hostClientFromFacade, + PluginInfo, + type PluginInfoService, + pluginInfoLayer, +} from "./host-client.js"; +export { loggingLayer } from "./logging.js"; export { atomicWriteFile, ensureStateDir, @@ -7,17 +20,6 @@ export { pluginStateDir, statePath, } from "./paths.js"; -export { - HostClient, - hostClientFromFacade, - type HostClientService, - PluginInfo, - pluginInfoLayer, - type PluginInfoService, -} from "./host-client.js"; -export { loggingLayer } from "./logging.js"; -export { type ConfigService, makeConfigService } from "./config.js"; -export { type CacheStore, makeCacheStore } from "./cache-store.js"; export { Artwork, DetectHint, @@ -33,6 +35,7 @@ export { type PluginKitDef, runPluginKitDirect, } from "./runtime.js"; +export { type SseRouteOptions, sseRoute } from "./sse.js"; export { type LastSync, makeSyncEngine, @@ -47,9 +50,7 @@ export { deriveConfigJsonSchema, httpApiEnv, makeConfigHandler, - serveUi, type ServeUiConfig, type ServeUiOptions, + serveUi, } from "./ui-server.js"; -export { sseRoute, type SseRouteOptions } from "./sse.js"; -export { type CliCommand, runPluginCli } from "./cli.js"; diff --git a/plugin-kit/src/library/define.ts b/plugin-kit/src/library/define.ts index 8978b12c..0ee87419 100644 --- a/plugin-kit/src/library/define.ts +++ b/plugin-kit/src/library/define.ts @@ -6,12 +6,13 @@ // appending launcher entries, serving `__config` so the console renders settings without the plugin // shipping an SPA, registering under `category: "library"` so it stays out of the nav, and the // standard CLI verbs. -import type { PluginDef } from "@punktfunk/host"; + import * as fs from "node:fs"; -import { Duration, Effect, Layer, Schema, Stream } from "effect"; +import type { PluginDef } from "@punktfunk/host"; +import { Duration, Effect, type Schema, Stream } from "effect"; import { type CliCommand, runPluginCli } from "../cli.js"; import { type ConfigService, makeConfigService } from "../config.js"; -import { HostClient, PluginInfo } from "../host-client.js"; +import { HostClient, type PluginInfo } from "../host-client.js"; import { ProviderClient, type ProviderClientService } from "../reconcile.js"; import { definePluginKit, type PluginKitDef } from "../runtime.js"; import { makeSyncEngine } from "../sync-engine.js"; @@ -103,8 +104,11 @@ export const defineLibraryPlugin = ( const debounce = def.debounce ?? Duration.seconds(3); /** The config service, built fresh wherever it is needed (it only requires `PluginInfo`). */ - const config: Effect.Effect, never, PluginInfo> = - makeConfigService({ schema: def.configSchema }); + const config: Effect.Effect< + ConfigService, + never, + PluginInfo + > = makeConfigService({ schema: def.configSchema }); /** Scan + launcher entries, in the order they should reach the host. */ const computeEntries = ( @@ -234,7 +238,8 @@ export const defineLibraryPlugin = ( }), }, scan: { - summary: "scan and print what WOULD be synced (--preview for the JSON entries)", + summary: + "scan and print what WOULD be synced (--preview for the JSON entries)", // Also offline: the point is to debug a scanner against real launcher files without // touching the host's library. offline: true, @@ -289,9 +294,9 @@ export const defineLibraryPlugin = ( } const baseline = yield* Effect.try({ try: () => - JSON.parse(fs.readFileSync(compare as string, "utf8")) as ReturnType< - typeof fromHostEntry - >[], + JSON.parse( + fs.readFileSync(compare as string, "utf8"), + ) as ReturnType[], catch: (cause) => new Error(`cannot read ${compare}: ${cause}`), }); const cfg = yield* (yield* config).load; @@ -307,7 +312,8 @@ export const defineLibraryPlugin = ( }), }, uninstall: { - summary: "remove this source's games from the host and release its store claim", + summary: + "remove this source's games from the host and release its store claim", run: () => Effect.gen(function* () { const provider = yield* ProviderClient; diff --git a/plugin-kit/src/library/parsers/art.ts b/plugin-kit/src/library/parsers/art.ts index f39ec22d..90cbdc88 100644 --- a/plugin-kit/src/library/parsers/art.ts +++ b/plugin-kit/src/library/parsers/art.ts @@ -36,7 +36,10 @@ export const fileUrl = (p: string): string => { * re-hashed; newer ones serve from an unpredictable per-asset-hash path, where this 404s and the * client falls through to its next candidate. That degradation is intentional and pre-existing. */ -export const steamCdnUrl = (appid: number, kind: ArtKind): string | undefined => { +export const steamCdnUrl = ( + appid: number, + kind: ArtKind, +): string | undefined => { // A non-Steam shortcut's appid has the high bit set and is never a real store appid — the CDN // would only 404, so don't emit a URL that is guaranteed to fail. if ((appid & 0x8000_0000) !== 0) return undefined; @@ -81,7 +84,12 @@ export const findLocalArtFile = ( } // Older Steam wrote the files directly under `librarycache/` with the appid in the name. for (const name of localFilenames(kind)) { - const flat = path.join(root, "appcache", "librarycache", `${appid}_${name}`); + const flat = path.join( + root, + "appcache", + "librarycache", + `${appid}_${name}`, + ); if (isFile(flat)) return flat; } return undefined; diff --git a/plugin-kit/src/library/parsers/fs.ts b/plugin-kit/src/library/parsers/fs.ts index 288f9243..a8e6c1a3 100644 --- a/plugin-kit/src/library/parsers/fs.ts +++ b/plugin-kit/src/library/parsers/fs.ts @@ -101,7 +101,7 @@ export const confinedJoin = (base: string, rel: string): string | undefined => { // Normalize separators so a Windows-shaped relative path is checked on any platform (a plugin // may parse a Windows manifest while its tests run on Linux). const parts = rel.split(/[\\/]/); - if (parts[0] === "" ) return undefined; // rooted + if (parts[0] === "") return undefined; // rooted if (/^[A-Za-z]:$/.test(parts[0])) return undefined; // drive prefix if (parts.some((p) => p === "..")) return undefined; // traversal const joined = path.join(base, ...parts.filter((p) => p !== "" && p !== ".")); diff --git a/plugin-kit/src/library/parsers/http.ts b/plugin-kit/src/library/parsers/http.ts index d1f629d9..db5daecb 100644 --- a/plugin-kit/src/library/parsers/http.ts +++ b/plugin-kit/src/library/parsers/http.ts @@ -6,8 +6,9 @@ // SSRF pivot from a process running on the operator's box (`http://169.254.169.254/…`, an internal // service). The host learned this in the 2026-07-17 security review; a plugin fetching the same // class of URL inherits the same rule. A rare legitimately-redirecting CDN just yields no art. -import { HostRequestError } from "../../errors.js"; + import { Effect } from "effect"; +import { HostRequestError } from "../../errors.js"; export interface FetchLimits { /** Hard cap on the response body. Default 8 MiB — a cover never approaches it. */ diff --git a/plugin-kit/src/library/parsers/shortcuts.ts b/plugin-kit/src/library/parsers/shortcuts.ts index b1348952..e343ff43 100644 --- a/plugin-kit/src/library/parsers/shortcuts.ts +++ b/plugin-kit/src/library/parsers/shortcuts.ts @@ -40,7 +40,10 @@ const readCStr = (buf: Uint8Array, c: Cursor): string | undefined => { /** Read a little-endian int32, advancing 4 bytes. `undefined` if fewer than 4 remain. */ const readI32 = (buf: Uint8Array, c: Cursor): number | undefined => { if (c.pos + 4 > buf.length) return undefined; - const v = new DataView(buf.buffer, buf.byteOffset + c.pos, 4).getInt32(0, true); + const v = new DataView(buf.buffer, buf.byteOffset + c.pos, 4).getInt32( + 0, + true, + ); c.pos += 4; return v; }; @@ -134,7 +137,7 @@ export const crc32 = (data: Uint8Array): number => { crc = (crc >>> 1) ^ (0xedb8_8320 & mask); } } - return (~crc) >>> 0; + return ~crc >>> 0; }; /** diff --git a/plugin-kit/src/logging.ts b/plugin-kit/src/logging.ts index 8f2f8d8c..0e76a3e7 100644 --- a/plugin-kit/src/logging.ts +++ b/plugin-kit/src/logging.ts @@ -2,12 +2,14 @@ // matching the format the scripting runner journals (and what the previous hand-rolled // plugin loggers emitted), so kit-based plugins read consistently in // `journalctl --user -u punktfunk-scripting`. -import { Cause, Layer, Logger } from "effect"; +import { Cause, type Layer, Logger } from "effect"; const render = (message: unknown): string => { if (typeof message === "string") return message; if (Array.isArray(message)) return message.map(render).join(" "); - return typeof message === "object" ? JSON.stringify(message) : String(message); + return typeof message === "object" + ? JSON.stringify(message) + : String(message); }; /** Replace the default logger with the runner-journal format. */ diff --git a/plugin-kit/src/react/index.tsx b/plugin-kit/src/react/index.tsx index 61f237c3..aeab2a49 100644 --- a/plugin-kit/src/react/index.tsx +++ b/plugin-kit/src/react/index.tsx @@ -7,9 +7,10 @@ // route init must read the last pathname segment — the hash is only a standalone-tab // fallback. Navigation posts `pf-ui:navigate` so the console mirrors the route into its // own URL (replace: true; the iframe src stays pinned — no reload loop). -import { useEffect, useState, type ReactNode } from "react"; + import { Option, Schema } from "effect"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; +import { type ReactNode, useEffect, useState } from "react"; /** `/plugin-ui/` when served through the console proxy, "" in dev/standalone. */ export const resolvePluginBase = (): string => { @@ -110,9 +111,7 @@ export interface ResultGateProps { * The one loading/error/success convention for plugin pages. Keeps showing the last * value while a refresh is in flight (no skeleton flash on invalidation). */ -export const ResultGate = ( - props: ResultGateProps, -): ReactNode => { +export const ResultGate = (props: ResultGateProps): ReactNode => { const { result } = props; if (AsyncResult.isSuccess(result)) return props.children(result.value); if (AsyncResult.isFailure(result)) { diff --git a/plugin-kit/src/reconcile.ts b/plugin-kit/src/reconcile.ts index 02dddd6c..391e922e 100644 --- a/plugin-kit/src/reconcile.ts +++ b/plugin-kit/src/reconcile.ts @@ -39,7 +39,9 @@ export interface ProviderClientService { * Remove every entry this provider owns **and release its store claim** (the explicit-uninstall * path). Releasing is what brings the host's built-in scanner back. */ - readonly remove: (providerId: string) => Effect.Effect; + readonly remove: ( + providerId: string, + ) => Effect.Effect; } export class ProviderClient extends Context.Service< diff --git a/plugin-kit/src/runtime.ts b/plugin-kit/src/runtime.ts index a2907ab0..341ce1ed 100644 --- a/plugin-kit/src/runtime.ts +++ b/plugin-kit/src/runtime.ts @@ -12,10 +12,10 @@ // plugin fiber (running scoped finalizers: UI deregistration, watcher close, cache flush) // and bounds the whole teardown with `shutdownGraceMs` so `main` always resolves. import { + connect, definePlugin, type PluginDef, type Punktfunk, - connect, } from "@punktfunk/host"; import { Cause, @@ -24,7 +24,7 @@ import { Fiber, Layer, ManagedRuntime, - Scope, + type Scope, } from "effect"; import { type HostClient, diff --git a/plugin-kit/src/sse.ts b/plugin-kit/src/sse.ts index 2a2d36f0..f91bde94 100644 --- a/plugin-kit/src/sse.ts +++ b/plugin-kit/src/sse.ts @@ -2,7 +2,7 @@ // beta.99), so the status feed is a raw HttpRouter route beside the HttpApi contract — // same wire shape the first-generation plugins used (`event: ` frames + comment // pings), which is already proven through the console's reverse proxy. -import { Effect, Layer, Schedule, Stream } from "effect"; +import { Effect, type Layer, Schedule, Stream } from "effect"; import { HttpRouter, HttpServerResponse } from "effect/unstable/http"; const encoder = new TextEncoder(); diff --git a/plugin-kit/src/sync-engine.ts b/plugin-kit/src/sync-engine.ts index d247356a..b4530fe5 100644 --- a/plugin-kit/src/sync-engine.ts +++ b/plugin-kit/src/sync-engine.ts @@ -13,7 +13,6 @@ import { createHash } from "node:crypto"; import * as fs from "node:fs"; import { - Cause, type Duration, Effect, Exit, @@ -40,7 +39,11 @@ export interface LastSync { } export type SyncOutcome = - | { readonly _tag: "Applied"; readonly report: Report; readonly count: number } + | { + readonly _tag: "Applied"; + readonly report: Report; + readonly count: number; + } | { readonly _tag: "Unchanged"; readonly report: Report } | { readonly _tag: "AlreadyRunning" }; @@ -278,8 +281,6 @@ export const makeSyncEngine = < status, changes: Stream.fromPubSub(hub), start: safeSync("startup").pipe(Effect.andThen(startLoops)), - reconfigure: startLoops.pipe( - Effect.andThen(safeSync("config-change")), - ), + reconfigure: startLoops.pipe(Effect.andThen(safeSync("config-change"))), } satisfies SyncEngine; }); diff --git a/plugin-kit/src/ui-server.ts b/plugin-kit/src/ui-server.ts index ca12e9b1..ddfb8723 100644 --- a/plugin-kit/src/ui-server.ts +++ b/plugin-kit/src/ui-server.ts @@ -3,7 +3,7 @@ // register/renew/deregister through Scope. Validated end-to-end by the phase-0 spike: // core-only env layers, no platform package, SPA fallthrough preserved. import { type PluginUiHandle, servePluginUi } from "@punktfunk/host"; -import { Effect, FileSystem, Layer, Path, Schema, Scope } from "effect"; +import { Effect, FileSystem, Layer, Path, Schema, type Scope } from "effect"; import { Etag, HttpPlatform, HttpRouter } from "effect/unstable/http"; import type { ConfigService } from "./config.js"; import { UiServeError } from "./errors.js"; @@ -176,7 +176,9 @@ export const serveUi = ( Effect.promise(() => dispose()).pipe(Effect.ignore), ); - const serveConfig = opts.config ? makeConfigHandler(opts.config) : undefined; + const serveConfig = opts.config + ? makeConfigHandler(opts.config) + : undefined; const fetch = async (req: Request): Promise => { const url = new URL(req.url); @@ -203,9 +205,7 @@ export const serveUi = ( ...(opts.staticDir !== undefined ? { staticDir: opts.staticDir } : {}), - ...(opts.category !== undefined - ? { category: opts.category } - : {}), + ...(opts.category !== undefined ? { category: opts.category } : {}), fetch, }), catch: (cause) => new UiServeError({ cause }), diff --git a/plugin-kit/test/config.test.ts b/plugin-kit/test/config.test.ts index 2ea2b95d..87d3665e 100644 --- a/plugin-kit/test/config.test.ts +++ b/plugin-kit/test/config.test.ts @@ -71,10 +71,7 @@ describe("ConfigService", () => { test("saveRaw persists the RAW shape verbatim (no defaults baked in)", async () => { await withService((svc) => svc.saveRaw({ roots: ["/roms"] })); const onDisk = JSON.parse( - fs.readFileSync( - path.join(pluginStateDir(PLUGIN), "config.json"), - "utf8", - ), + fs.readFileSync(path.join(pluginStateDir(PLUGIN), "config.json"), "utf8"), ); expect(onDisk).toEqual({ roots: ["/roms"] }); // no sync block materialized const loaded = await withService((svc) => svc.load); diff --git a/plugin-kit/test/library-config.test.ts b/plugin-kit/test/library-config.test.ts index 9c398bf0..a07925d3 100644 --- a/plugin-kit/test/library-config.test.ts +++ b/plugin-kit/test/library-config.test.ts @@ -25,7 +25,10 @@ const ScannerConfig = Schema.Struct({ }), ), root: Schema.optionalKey( - Schema.String.annotate({ title: "Launcher root", description: "Absolute path." }), + Schema.String.annotate({ + title: "Launcher root", + description: "Absolute path.", + }), ), extraRoots: Schema.Array(Schema.String) .annotate({ title: "Extra roots" }) @@ -36,7 +39,10 @@ const ScannerConfig = Schema.Struct({ ), ), launchers: Schema.Struct({ - bigpicture: Schema.Boolean.annotate({ title: "Big Picture", default: true }), + bigpicture: Schema.Boolean.annotate({ + title: "Big Picture", + default: true, + }), desktop: Schema.Boolean.annotate({ title: "Desktop", default: false }), }).pipe( Schema.withDecodingDefaultKey( @@ -80,7 +86,10 @@ describe("S2 — JSON Schema derivation for __config", () => { // A nested object stays nested — the form renders a fieldset, not a JSON blob. expect(p.launchers).toMatchObject({ type: "object", - properties: { bigpicture: { type: "boolean" }, desktop: { type: "boolean" } }, + properties: { + bigpicture: { type: "boolean" }, + desktop: { type: "boolean" }, + }, }); // A literal union derives a clean enum — prefer it over a union of strings. expect(p.artSource).toMatchObject({ @@ -92,7 +101,9 @@ describe("S2 — JSON Schema derivation for __config", () => { test("annotations pass through — they are the ONLY source of labels and defaults", () => { const p = props(); expect(p.enabled.title).toBe("Enable scanning"); - expect(p.enabled.description).toBe("Whether this source contributes titles."); + expect(p.enabled.description).toBe( + "Whether this source contributes titles.", + ); // The derivation does NOT infer `default` from withDecodingDefaultKey, so an un-annotated // field shows the form no placeholder at all. Annotate every field. expect(p.enabled.default).toBe(true); @@ -120,13 +131,13 @@ describe("S2 — JSON Schema derivation for __config", () => { expect(props().pollMinutes).toMatchObject({ type: "integer" }); // The trap, pinned: Schema.Number's ENCODED form admits "NaN"/"Infinity"/"-Infinity", so it // derives a four-way anyOf that no number input can render. Use Finite or Int. - const bad = deriveConfigJsonSchema( - Schema.Struct({ n: Schema.Number }), - ) as { schema: { properties: { n: { anyOf?: unknown[] } } } }; + const bad = deriveConfigJsonSchema(Schema.Struct({ n: Schema.Number })) as { + schema: { properties: { n: { anyOf?: unknown[] } } }; + }; expect(Array.isArray(bad.schema.properties.n.anyOf)).toBe(true); - const ok = deriveConfigJsonSchema( - Schema.Struct({ n: Schema.Finite }), - ) as { schema: { properties: { n: { type?: string } } } }; + const ok = deriveConfigJsonSchema(Schema.Struct({ n: Schema.Finite })) as { + schema: { properties: { n: { type?: string } } }; + }; expect(ok.schema.properties.n.type).toBe("number"); }); @@ -141,7 +152,10 @@ describe("S2 — JSON Schema derivation for __config", () => { describe("__config wire contract", () => { const withService = async ( - use: (handler: (req: Request) => Promise, file: string) => Promise, + use: ( + handler: (req: Request) => Promise, + file: string, + ) => Promise, ): Promise => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pf-kit-cfg-")); const prev = process.env.PUNKTFUNK_CONFIG_DIR; @@ -150,7 +164,9 @@ describe("__config wire contract", () => { const service = await Effect.runPromise( makeConfigService({ schema: ScannerConfig }).pipe( Effect.provide( - Layer.mergeAll(pluginInfoLayer({ name: "steam", version: "0.1.0" })), + Layer.mergeAll( + pluginInfoLayer({ name: "steam", version: "0.1.0" }), + ), ), ), ); diff --git a/plugin-kit/test/library-parity.test.ts b/plugin-kit/test/library-parity.test.ts index 2abc3f5a..641918ea 100644 --- a/plugin-kit/test/library-parity.test.ts +++ b/plugin-kit/test/library-parity.test.ts @@ -40,7 +40,8 @@ const pluginEntry = (over: Partial = {}): ProviderEntry => art: { portrait: "file:///home/u/.steam/appcache/librarycache/440/a/p.jpg", hero: "https://cdn.cloudflare.steamstatic.com/steam/apps/440/library_hero.jpg", - header: "https://cdn.cloudflare.steamstatic.com/steam/apps/440/header.jpg", + header: + "https://cdn.cloudflare.steamstatic.com/steam/apps/440/header.jpg", }, platform: "PC", ...over, diff --git a/plugin-kit/test/spike-client-prefix.test.ts b/plugin-kit/test/spike-client-prefix.test.ts index 71fb37e0..7b5d468e 100644 --- a/plugin-kit/test/spike-client-prefix.test.ts +++ b/plugin-kit/test/spike-client-prefix.test.ts @@ -45,9 +45,7 @@ describe("spike 2: client prefix through the console proxy", () => { const captured: Array = []; class Api extends AtomHttpApi.Service()("SpikeApiPrepend", { api, - httpClient: Layer.succeed(HttpClient.HttpClient)( - captureClient(captured), - ), + httpClient: Layer.succeed(HttpClient.HttpClient)(captureClient(captured)), transformClient: HttpClient.mapRequest( HttpClientRequest.prependUrl(PREFIX), ), @@ -66,9 +64,7 @@ describe("spike 2: client prefix through the console proxy", () => { const captured: Array = []; class Api extends AtomHttpApi.Service()("SpikeApiBaseUrl", { api, - httpClient: Layer.succeed(HttpClient.HttpClient)( - captureClient(captured), - ), + httpClient: Layer.succeed(HttpClient.HttpClient)(captureClient(captured)), baseUrl: PREFIX, }) {} diff --git a/plugin-kit/test/spike-httpapi.test.ts b/plugin-kit/test/spike-httpapi.test.ts index a9435f94..dba8e229 100644 --- a/plugin-kit/test/spike-httpapi.test.ts +++ b/plugin-kit/test/spike-httpapi.test.ts @@ -9,6 +9,8 @@ // 3. The real servePluginUi server (loopback, per-boot bearer secret, __health) proxies // into the HttpApi handler end-to-end. import { describe, expect, test } from "bun:test"; +import type { Punktfunk } from "@punktfunk/host"; +import { servePluginUi } from "@punktfunk/host"; import { Effect, Layer, Schema } from "effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -19,8 +21,6 @@ import { HttpApiEndpoint, HttpApiGroup, } from "effect/unstable/httpapi"; -import { servePluginUi } from "@punktfunk/host"; -import type { Punktfunk } from "@punktfunk/host"; const Pong = Schema.Struct({ ok: Schema.Boolean, source: Schema.String }); const EchoIn = Schema.Struct({ msg: Schema.String }); @@ -90,8 +90,11 @@ describe("spike 1: HttpApi via toWebHandler on Bun", () => { test("end-to-end behind servePluginUi (loopback + bearer secret)", async () => { const { handler, dispose } = HttpRouter.toWebHandler(appLayer); - const registrations: Array<{ method: string; path: string; body: unknown }> = - []; + const registrations: Array<{ + method: string; + path: string; + body: unknown; + }> = []; // servePluginUi only touches pf.request — a recording stub is a faithful host. const pf = { request: async (method: string, path: string, body?: unknown) => { @@ -116,15 +119,18 @@ describe("spike 1: HttpApi via toWebHandler on Bun", () => { (r) => r.method === "PUT" && r.path === "/plugins/spike", ); expect(reg).toBeDefined(); - const secret = (reg?.body as { ui: { secret: string } }).ui.secret; + // Not `reg?.body`: the optional chain undoes the assertion above — if `reg` were + // undefined the `.ui` access would throw a TypeError instead of failing this test + // readably. The `expect` is what guarantees it, so assert it to the type system too. + if (!reg) throw new Error("registration not found"); + const secret = (reg.body as { ui: { secret: string } }).ui.secret; expect(secret.length).toBeGreaterThanOrEqual(16); const auth = { authorization: `Bearer ${secret}` }; // Health endpoint is served by servePluginUi itself. - const health = await fetch( - `http://127.0.0.1:${ui.port}/__health`, - { headers: auth }, - ); + const health = await fetch(`http://127.0.0.1:${ui.port}/__health`, { + headers: auth, + }); expect(health.status).toBe(200); // HttpApi endpoint through the real server. diff --git a/plugin-kit/test/sse-live.test.ts b/plugin-kit/test/sse-live.test.ts index 65d08f7e..b7b1be53 100644 --- a/plugin-kit/test/sse-live.test.ts +++ b/plugin-kit/test/sse-live.test.ts @@ -44,7 +44,9 @@ describe("sseRoute (live, PubSub-backed)", () => { const { handler, dispose } = HttpRouter.toWebHandler( Layer.provide(routes, httpApiEnv), ); - const res = yield* Effect.promise(() => handler(new Request("http://127.0.0.1/api/events"))); + const res = yield* Effect.promise(() => + handler(new Request("http://127.0.0.1/api/events")), + ); expect(res.status).toBe(200); // Publish only once the response is open — the real engine's pattern. setTimeout(() => { @@ -68,7 +70,9 @@ describe("sseRoute (live, PubSub-backed)", () => { const { handler, dispose } = HttpRouter.toWebHandler( Layer.provide(routes, httpApiEnv), ); - const res = yield* Effect.promise(() => handler(new Request("http://127.0.0.1/api/events"))); + const res = yield* Effect.promise(() => + handler(new Request("http://127.0.0.1/api/events")), + ); const body = yield* Effect.promise(() => readSome(res, 4000)); yield* Effect.promise(() => dispose()); return body; diff --git a/plugin-kit/test/sse.test.ts b/plugin-kit/test/sse.test.ts index 7c847093..27c2074f 100644 --- a/plugin-kit/test/sse.test.ts +++ b/plugin-kit/test/sse.test.ts @@ -18,13 +18,9 @@ describe("sseRoute", () => { Layer.provide(routes, httpApiEnv), ); try { - const res = await handler( - new Request("http://127.0.0.1/api/events"), - ); + const res = await handler(new Request("http://127.0.0.1/api/events")); expect(res.status).toBe(200); - expect(res.headers.get("content-type")).toContain( - "text/event-stream", - ); + expect(res.headers.get("content-type")).toContain("text/event-stream"); const text = await res.text(); expect(text).toContain('event: status\ndata: {"tick":0}\n\n'); expect(text).toContain('event: status\ndata: {"tick":2}\n\n'); diff --git a/plugin-kit/test/sync-engine.test.ts b/plugin-kit/test/sync-engine.test.ts index d53936d1..a5a93e76 100644 --- a/plugin-kit/test/sync-engine.test.ts +++ b/plugin-kit/test/sync-engine.test.ts @@ -1,6 +1,6 @@ // SyncEngine semantics: fingerprint skip, single-flight coalescing, status feed. import { describe, expect, test } from "bun:test"; -import { Duration, Effect, Fiber, Ref, Scope, Stream } from "effect"; +import { Duration, Effect, Fiber, Ref, type Scope, Stream } from "effect"; import { type LastSync, makeSyncEngine, @@ -19,33 +19,31 @@ const harness = (opts?: { const applied = yield* Ref.make(0); const last = yield* Ref.make(undefined); const entries = opts?.entries ?? (() => ["a", "b"]); - const engine = yield* makeSyncEngine, never>( - { - compute: () => - Effect.suspend(() => { - const e = entries(); - return Effect.succeed({ - entries: e, - report: { included: e.length }, - }); - }).pipe( - opts?.computeDelayMs - ? Effect.delay(Duration.millis(opts.computeDelayMs)) - : (x) => x, - ), - apply: () => Ref.update(applied, (n) => n + 1), - lastSync: { - get: Ref.get(last), - set: (l) => Ref.set(last, l), - }, - settings: Effect.succeed({ - pollInterval: Duration.minutes(60), - watch: false, - debounce: Duration.millis(10), - watchDirs: [], - }), + const engine = yield* makeSyncEngine, never>({ + compute: () => + Effect.suspend(() => { + const e = entries(); + return Effect.succeed({ + entries: e, + report: { included: e.length }, + }); + }).pipe( + opts?.computeDelayMs + ? Effect.delay(Duration.millis(opts.computeDelayMs)) + : (x) => x, + ), + apply: () => Ref.update(applied, (n) => n + 1), + lastSync: { + get: Ref.get(last), + set: (l) => Ref.set(last, l), }, - ); + settings: Effect.succeed({ + pollInterval: Duration.minutes(60), + watch: false, + debounce: Duration.millis(10), + watchDirs: [], + }), + }); return { engine, applied, last }; }); From 4edb662b63338132fcad6c138a7cf815b0cb09c7 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 8 Aug 2026 02:19:30 +0200 Subject: [PATCH 4/6] fix(plugin-kit): regSubKeys could never return a subkey MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found on hardware by the GOG plugin's own parity gate, on a box with exactly one GOG game installed: HKLM\SOFTWARE\WOW6432Node\GOG.com\Games -> 1 subkey (IRON NEST ...) host's built-in scanner: 1 entry plugin: detect: absent, 0 games parity FAILED - 1 missing, exit 1 `reg.exe` ALWAYS echoes the full hive name in its output rows, never the abbreviation it was given: query `HKLM\SOFTWARE\...` and every line comes back `HKEY_LOCAL_MACHINE\SOFTWARE\...`. regSubKeys built its match prefix from the `HKLM\...` string it was handed, so no line ever matched and it returned `[]` — on every machine, for every key, always. Measured verbatim on .173: reg.exe: [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\GOG.com\Games\2013434102] regSubKeys: [] Its only consumer is the GOG plugin, so the symptom was "GOG reports no games installed" rather than an error — the same shape as the SQLite reader in 0.3.1: a total failure that every layer degrades into an empty library. The contract was wrong too, and the hive bug hid it. regSubKeys returned whole key PATHS while the GOG plugin uses each result as a bare NAME (`const key = \`${GAMES_KEY}\\${id}\``, and the subkey name IS the product id that becomes `external_id`). Even with the prefix fixed, paths would have composed nonsense keys. It now returns names, which is what the sole consumer and its own comment always assumed. Parsing is split into an exported `parseRegSubKeys(stdout, key)` for the same reason `parseRegQuery` is exported — this is a text format that breaks quietly, and it had NO test coverage at all. Six added, using the verbatim .173 output: names not paths, multiple subkeys, grandchildren ignored, the queried key is not its own subkey, case-insensitivity, and empty/error input. Four of the six FAIL against the old behaviour. 0.3.1 -> 0.3.2. Gates: biome clean, tsc clean, 67/67 tests, build clean. --- plugin-kit/package.json | 2 +- plugin-kit/src/library/parsers/index.ts | 5 +- plugin-kit/src/library/parsers/registry.ts | 43 +++++-- plugin-kit/test/library-parsers.test.ts | 134 ++++++++++++++++++--- 4 files changed, 154 insertions(+), 30 deletions(-) diff --git a/plugin-kit/package.json b/plugin-kit/package.json index 4906d1f5..d1058736 100644 --- a/plugin-kit/package.json +++ b/plugin-kit/package.json @@ -1,6 +1,6 @@ { "name": "@punktfunk/plugin-kit", - "version": "0.3.1", + "version": "0.3.2", "description": "Effect-based framework for punktfunk plugins: lifecycle runtime, config/state, sync engine, UI serving, CLI scaffold, and browser helpers.", "type": "module", "license": "MIT OR Apache-2.0", diff --git a/plugin-kit/src/library/parsers/index.ts b/plugin-kit/src/library/parsers/index.ts index 697f2f12..2a20f2dc 100644 --- a/plugin-kit/src/library/parsers/index.ts +++ b/plugin-kit/src/library/parsers/index.ts @@ -32,13 +32,13 @@ export { } from "./http.js"; export { parseRegQuery, + parseRegSubKeys, + type RegValue, regQueryValue, regQueryValues, regSubKeys, - type RegValue, validRegKey, } from "./registry.js"; -export { openReadOnly, type ReadOnlyDb, withReadOnlyDb } from "./sqlite.js"; export { crc32, parseShortcuts, @@ -46,6 +46,7 @@ export { shortcutAppId, shortcutGameId, } from "./shortcuts.js"; +export { openReadOnly, type ReadOnlyDb, withReadOnlyDb } from "./sqlite.js"; export { steamLibraryDirs, steamRoots, diff --git a/plugin-kit/src/library/parsers/registry.ts b/plugin-kit/src/library/parsers/registry.ts index 4b420d66..5ebafe88 100644 --- a/plugin-kit/src/library/parsers/registry.ts +++ b/plugin-kit/src/library/parsers/registry.ts @@ -59,17 +59,46 @@ export const regQueryValue = (key: string, name: string): string | undefined => regQueryValues(key).find((v) => v.name.toLowerCase() === name.toLowerCase()) ?.data; -/** The immediate SUBKEY paths under one HKLM key (GOG lists one subkey per installed game). */ +/** + * `reg.exe` always echoes the FULL hive name in its output rows, never the abbreviation it was + * given: query `HKLM\SOFTWARE\…` and every line comes back `HKEY_LOCAL_MACHINE\SOFTWARE\…`. + */ +const HKLM_FULL = "HKEY_LOCAL_MACHINE\\"; + +/** + * Parse `reg.exe query ` output into the immediate subkey NAMES under `key`. + * + * Exported for tests, like {@link parseRegQuery}, and for the same reason — this is a text format + * that quietly breaks, and it did: the previous version matched output lines against the + * abbreviated `HKLM\…` prefix it was handed, while reg.exe prints `HKEY_LOCAL_MACHINE\…`. Nothing + * ever matched, so it returned `[]` on every machine, forever, and the one plugin that uses it + * (GOG) reported "no games installed" instead of failing. See the regSubKeys tests. + * + * Returns NAMES, not paths: the sole consumer composes `${key}\\${name}`, and a GOG subkey name IS + * the product id that becomes the entry's `external_id`. + */ +export const parseRegSubKeys = (stdout: string, key: string): string[] => { + const full = key.toUpperCase().startsWith(HKLM) + ? HKLM_FULL + key.slice(HKLM.length) + : key; + const prefix = `${full.toLowerCase()}\\`; + return ( + stdout + .split(/\r?\n/) + .map((l) => l.trim()) + .filter((l) => l.toLowerCase().startsWith(prefix)) + .map((l) => l.slice(full.length + 1)) + // Immediate children only — a deeper path still starts with the prefix. + .filter((name) => name !== "" && !name.includes("\\")) + ); +}; + +/** The immediate SUBKEY NAMES under one HKLM key (GOG lists one subkey per installed game). */ export const regSubKeys = (key: string): string[] => { if (!validRegKey(key)) return []; const out = run(["query", key]); if (out === undefined) return []; - const prefix = `${key.toLowerCase()}\\`; - return out - .split(/\r?\n/) - .map((l) => l.trim()) - .filter((l) => l.toLowerCase().startsWith(prefix)) - .filter((l) => !l.slice(key.length + 1).includes("\\")); + return parseRegSubKeys(out, key); }; /** diff --git a/plugin-kit/test/library-parsers.test.ts b/plugin-kit/test/library-parsers.test.ts index ae290c7d..55ead4bf 100644 --- a/plugin-kit/test/library-parsers.test.ts +++ b/plugin-kit/test/library-parsers.test.ts @@ -5,23 +5,24 @@ // and launches nothing. Where a Rust test exists, its assertions are carried over verbatim — the // per-plugin parity harness (design M5) then checks the whole pipeline against a live host, but // these catch a drift long before that. -import { describe, expect, test } from "bun:test"; + import { Database } from "bun:sqlite"; +import { describe, expect, test } from "bun:test"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { confinedJoin, crc32, + fileUrl, findGridArtFile, findLocalArtFile, - fileUrl, gridFilenames, isSteamTool, - withReadOnlyDb, openReadOnly, parseAppManifest, parseRegQuery, + parseRegSubKeys, parseShortcuts, readTextCapped, shortcutAppId, @@ -29,6 +30,7 @@ import { steamCdnUrl, vdfPaths, vdfValue, + withReadOnlyDb, } from "../src/library/parsers/index.js"; const tmp = (name: string): string => { @@ -85,7 +87,9 @@ describe("text VDF / ACF", () => { }); test("isSteamTool keeps runtimes out of a game library", () => { - expect(isSteamTool(228980, "Steamworks Common Redistributables")).toBe(true); + expect(isSteamTool(228980, "Steamworks Common Redistributables")).toBe( + true, + ); expect(isSteamTool(1628350, "Steam Linux Runtime 3.0 (sniper)")).toBe(true); expect(isSteamTool(999, "Proton 9.0")).toBe(true); expect(isSteamTool(999, "SteamVR")).toBe(true); @@ -109,7 +113,12 @@ describe("binary shortcuts.vdf", () => { parts.push(0); }; const i32 = (v: number) => { - parts.push(v & 0xff, (v >>> 8) & 0xff, (v >>> 16) & 0xff, (v >>> 24) & 0xff); + parts.push( + v & 0xff, + (v >>> 8) & 0xff, + (v >>> 16) & 0xff, + (v >>> 24) & 0xff, + ); }; parts.push(0x00); cstr("shortcuts"); @@ -205,9 +214,13 @@ describe("path confinement", () => { path.join(base, "bin", "game.exe"), ); // The three shapes a crafted goggame-*.info would use to point elsewhere. - expect(confinedJoin(base, "../../windows/system32/cmd.exe")).toBeUndefined(); + expect( + confinedJoin(base, "../../windows/system32/cmd.exe"), + ).toBeUndefined(); expect(confinedJoin(base, "/etc/passwd")).toBeUndefined(); - expect(confinedJoin(base, "C:\\Windows\\system32\\cmd.exe")).toBeUndefined(); + expect( + confinedJoin(base, "C:\\Windows\\system32\\cmd.exe"), + ).toBeUndefined(); expect(confinedJoin(base, "")).toBeUndefined(); }); }); @@ -237,8 +250,14 @@ describe("art locations", () => { test("grid filenames follow Steam's per-kind naming", () => { expect(gridFilenames(570, "portrait")).toEqual(["570p.png", "570p.jpg"]); - expect(gridFilenames(570, "hero")).toEqual(["570_hero.png", "570_hero.jpg"]); - expect(gridFilenames(570, "logo")).toEqual(["570_logo.png", "570_logo.jpg"]); + expect(gridFilenames(570, "hero")).toEqual([ + "570_hero.png", + "570_hero.jpg", + ]); + expect(gridFilenames(570, "logo")).toEqual([ + "570_logo.png", + "570_logo.jpg", + ]); expect(gridFilenames(570, "header")).toEqual(["570.png", "570.jpg"]); }); @@ -307,8 +326,12 @@ describe("openReadOnly", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pf-kit-sqlite-")); const file = path.join(dir, "pga.db"); const seed = new Database(file); - seed.run("CREATE TABLE games (id INTEGER PRIMARY KEY, name TEXT, installed INT)"); - seed.run("INSERT INTO games (id, name, installed) VALUES (1, 'Ubisoft Connect', 1)"); + seed.run( + "CREATE TABLE games (id INTEGER PRIMARY KEY, name TEXT, installed INT)", + ); + seed.run( + "INSERT INTO games (id, name, installed) VALUES (1, 'Ubisoft Connect', 1)", + ); seed.close(); try { return use(file); @@ -321,9 +344,9 @@ describe("openReadOnly", () => { withDb((file) => { const db = openReadOnly(file); expect(db).toBeDefined(); - expect(db?.query("SELECT id, name FROM games WHERE installed = 1")).toEqual([ - { id: 1, name: "Ubisoft Connect" }, - ]); + expect( + db?.query("SELECT id, name FROM games WHERE installed = 1"), + ).toEqual([{ id: 1, name: "Ubisoft Connect" }]); db?.close(); }); }); @@ -337,7 +360,9 @@ describe("openReadOnly", () => { seed.run("INSERT INTO games (id) VALUES (7)"); seed.close(); try { - expect(openReadOnly(file)?.query("SELECT id FROM games")).toEqual([{ id: 7 }]); + expect(openReadOnly(file)?.query("SELECT id FROM games")).toEqual([ + { id: 7 }, + ]); } finally { fs.rmSync(dir, { recursive: true, force: true }); } @@ -345,16 +370,20 @@ describe("openReadOnly", () => { test("withReadOnlyDb reads, then closes", () => { withDb((file) => { - expect(withReadOnlyDb(file, (h) => h.query("SELECT name FROM games"))).toEqual([ - { name: "Ubisoft Connect" }, - ]); + expect( + withReadOnlyDb(file, (h) => h.query("SELECT name FROM games")), + ).toEqual([{ name: "Ubisoft Connect" }]); }); }); // The "not installed" contract — an absent file is `undefined`, never a throw. test("absent file is undefined, not an error", () => { - expect(openReadOnly(path.join(os.tmpdir(), "pf-kit-nope", "pga.db"))).toBeUndefined(); - expect(withReadOnlyDb(path.join(os.tmpdir(), "pf-kit-nope", "pga.db"), () => 1)).toBeUndefined(); + expect( + openReadOnly(path.join(os.tmpdir(), "pf-kit-nope", "pga.db")), + ).toBeUndefined(); + expect( + withReadOnlyDb(path.join(os.tmpdir(), "pf-kit-nope", "pga.db"), () => 1), + ).toBeUndefined(); }); // Schema drift degrades to no rows rather than taking the plugin down. @@ -366,3 +395,68 @@ describe("openReadOnly", () => { }); }); }); + +// Subkey enumeration, against the output reg.exe ACTUALLY prints. +// +// This had no coverage and was broken end to end: it matched lines against the abbreviated +// `HKLM\…` prefix it was handed, but reg.exe echoes `HKEY_LOCAL_MACHINE\…`. Nothing ever matched, +// so it returned [] on every machine, and the GOG plugin — its only consumer — reported "no games +// installed" rather than failing. Caught on hardware by the parity gate: the host's built-in +// scanner found IRON NEST, the plugin found nothing. +// +// The fixture is the verbatim output from .173 (a blank line, then one subkey row). +describe("parseRegSubKeys", () => { + const KEY = "HKLM\\SOFTWARE\\WOW6432Node\\GOG.com\\Games"; + + test("returns subkey NAMES from real reg.exe output", () => { + const stdout = [ + "", + "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\GOG.com\\Games\\2013434102", + "", + ].join("\r\n"); + // The name is the GOG product id, and the consumer composes `${KEY}\\${name}`. + expect(parseRegSubKeys(stdout, KEY)).toEqual(["2013434102"]); + }); + + test("several subkeys, in order", () => { + const base = "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\GOG.com\\Games"; + const stdout = ["", `${base}\\1207658930`, `${base}\\2013434102`].join( + "\r\n", + ); + expect(parseRegSubKeys(stdout, KEY)).toEqual(["1207658930", "2013434102"]); + }); + + // reg.exe /s output nests deeper; only immediate children are subkeys of this key. + test("ignores grandchildren", () => { + const base = "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\GOG.com\\Games"; + const stdout = [ + "", + `${base}\\2013434102`, + `${base}\\2013434102\\tasks`, + ].join("\r\n"); + expect(parseRegSubKeys(stdout, KEY)).toEqual(["2013434102"]); + }); + + // The queried key itself is echoed as a header when it has values; it is not its own subkey. + test("does not return the queried key itself", () => { + const stdout = [ + "", + "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\GOG.com\\Games", + "", + ].join("\r\n"); + expect(parseRegSubKeys(stdout, KEY)).toEqual([]); + }); + + test("case-insensitive on the hive and path", () => { + const stdout = + "hkey_local_machine\\software\\wow6432node\\gog.com\\games\\42"; + expect(parseRegSubKeys(stdout, KEY)).toEqual(["42"]); + }); + + test("no subkeys is empty, not a throw", () => { + expect(parseRegSubKeys("", KEY)).toEqual([]); + expect( + parseRegSubKeys("ERROR: The system was unable to find...", KEY), + ).toEqual([]); + }); +}); From b1e05258727b2abd5a33551a03432e14223a3534 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 8 Aug 2026 02:34:59 +0200 Subject: [PATCH 5/6] fix(packaging/arch): pacman could upgrade FFmpeg out from under the host and brick it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `depends=('ffmpeg' ...)` carried no version bound, and pacman is the only one of our packaging formats that does not derive dependencies from ELF DT_NEEDED — rpm auto-generates `libavcodec.so.62()(64bit)`, dpkg-shlibdeps emits `libavcodec62`, nix pins the closure. So when Arch shipped ffmpeg 2:9.0-5 on 2026-08-08 and every soname moved (libavutil .60->.61, libavcodec .62->.63, libavfilter .11->.12, libavdevice .62->.63, libswscale .9->.10), a plain `pacman -Syu` walked every Arch/CachyOS install straight across the break. The result is not a crash we can log: the dynamic loader cannot start the binary at all, so it is exit 127 *before* main() in a systemd restart loop, and because punktfunk-web is a separate bun service with no libav linkage it keeps serving happily while :47990 has nothing listening — which reads as "the mgmt API is broken" rather than "the host is not running". `ldd /usr/bin/punktfunk-host | grep "not found"` is the one-line diagnosis. Depend on the sonames instead of the package. Arch's ffmpeg declares the matching `provides=(libavcodec.so=63-64 ...)`, and makepkg rewrites each bare `libfoo.so` listed in depends into `libfoo.so=-` by reading the built binary's DT_NEEDED, so the bound tracks whatever FFmpeg the builder linked against with nothing to hand-maintain across the next bump. pacman now refuses the ffmpeg upgrade rather than bricking the install. A hand-written `ffmpeg<2:9` would have gone stale on the very next major; not bundling FFmpeg the way the .deb does, because that exists only because Ubuntu 24.04 LTS is frozen on 6.1 and can never satisfy the dep, while rolling Arch always ships a current one. Verified on a real ffmpeg-9 box (192.168.1.21): the built package records libavcodec.so=63-64, libavutil.so=61-64, libavfilter.so=12-64, libavdevice.so=63-64 and libswscale.so=10-64, exactly matching DT_NEEDED, with the two libs --as-needed drops left bare and satisfied by any ffmpeg. The new arch.yml step asserts that expansion actually happened. If it ever stops — Arch dropping the soname provides, someone tidying the entries out of depends — the dep silently degrades to an unversioned name that any ffmpeg satisfies, which is exactly the state that caused this, and it is invisible in a green build until a box bricks weeks later. --- .gitea/workflows/arch.yml | 25 +++++++++++++++++++++++++ packaging/arch/PKGBUILD | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/arch.yml b/.gitea/workflows/arch.yml index f46c1f59..02567797 100644 --- a/.gitea/workflows/arch.yml +++ b/.gitea/workflows/arch.yml @@ -173,6 +173,31 @@ jobs: makepkg -f -d --holdver ls -lh "$GITHUB_WORKSPACE/dist" + # The host must ship a VERSIONED libav soname dep, and nothing else in this pipeline proves + # it. packaging/arch/PKGBUILD lists bare `libavcodec.so` etc. and relies on makepkg rewriting + # each into `libavcodec.so=-` from the built binary's DT_NEEDED; if that + # rewrite ever stops happening — Arch dropping the soname `provides`, someone "tidying" the + # entries out of `depends`, a makepkg change — the dep silently degrades to an unversioned + # name that ANY ffmpeg satisfies. That is precisely the 2026-08-08 state in which `pacman + # -Syu` walked every Arch/CachyOS install across the FFmpeg 8 -> 9 soname bump and left the + # host unable to start (exit 127 before main(), restart loop). The failure is invisible in a + # green build and only shows up as a bricked box weeks later, so assert it here. + - name: Assert the host pins the FFmpeg soname + run: | + PKG="$(ls "$GITHUB_WORKSPACE"/dist/punktfunk-host-*.pkg.tar.zst | head -1)" + DEPS="$(bsdtar -xOf "$PKG" .PKGINFO | sed -n 's/^depend = //p')" + echo "$DEPS" | sed 's/^/ depend = /' + for lib in libavcodec libavutil; do + echo "$DEPS" | grep -qE "^$lib\.so=[0-9]+-[0-9]+$" || { + echo "::error::punktfunk-host declares no VERSIONED $lib.so dependency." + echo "::error::makepkg did not expand the bare soname from DT_NEEDED, so pacman can" + echo "::error::upgrade FFmpeg across a soname break and brick the install." + echo "::error::See the depends comment in packaging/arch/PKGBUILD." + exit 1 + } + done + echo "OK: $(echo "$DEPS" | grep -E '^libav|^libsw' | tr '\n' ' ')" + # The optional HDR gamescope companion (packaging/gamescope) — a separate pkgbase with a # completely different dependency set, published into the same repo so `pacman -S # punktfunk-gamescope` is all an Arch/SteamOS box needs for 10-bit BT.2020 PQ. diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 1eff3ca8..b2892318 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -47,7 +47,11 @@ options=('!lto' '!debug') # All build deps for both packages (Arch runtime packages ship their own headers, so these cover # build + link). aws-lc/ring need clang+cmake; nasm is for asm. ffmpeg stays because the HOST's -# encoder links libav* — the CLIENT dropped it in M10 and decodes natively. No vulkan-headers: +# encoder links libav* — the CLIENT dropped it in M10 and decodes natively. `ffmpeg` is +# deliberately UNVERSIONED here: ffmpeg-sys-next auto-detects the installed FFmpeg, so the package +# builds against whatever the builder ships, and the RUNTIME bound follows automatically from the +# soname deps makepkg derives from the linked binary (see package_punktfunk-host's depends). Pinning +# a version at build time would just have to be re-edited on every FFmpeg major. No vulkan-headers: # nothing in the workspace compiles against the system Vulkan headers any more (pyrowave-sys builds # against its own vendored copy, and both binaries reach Vulkan through ash, which dlopens it). makedepends=('rust' 'cargo' 'clang' 'cmake' 'nasm' 'pkgconf' 'git' @@ -135,8 +139,33 @@ package_punktfunk-host() { # opens a Pulse socket itself, so pipewire-pulse is an OPTdepend, not a depend: it exists # for the GAMES, which commonly emit through the PulseAudio API. Hard-depending on it made # the package uninstallable next to real `pulseaudio`, which serves those games just as well. + # ⚠ The libav* entries below are SONAME deps, not package names, and they are load-bearing. + # pacman is the only one of our packaging formats that does NOT derive dependencies from ELF + # DT_NEEDED (rpm auto-generates `libavcodec.so.62()(64bit)`; dpkg-shlibdeps emits `libavcodec62`; + # nix pins the closure). So a bare `depends=('ffmpeg')` let `pacman -Syu` walk the host across an + # FFmpeg soname bump with no warning and no conflict — which is exactly what FFmpeg 8 -> 9 + # (2026-08-08, ffmpeg 2:9.0-5: libavutil .60->.61, libavcodec .62->.63, libavfilter .11->.12, + # libavdevice .62->.63, libswscale .9->.10) did to every Arch/CachyOS user: the dynamic loader + # cannot start the binary at all, so it is exit 127 *before* main() in a systemd restart loop, + # with nothing in the host's own log to explain it (`ldd /usr/bin/punktfunk-host | grep "not found"` + # is the one-line diagnosis). Arch's ffmpeg declares the matching + # `provides=(libavcodec.so=63-64 libavutil.so=61-64 ...)`, and makepkg rewrites each bare + # `libfoo.so` listed here into `libfoo.so=-` by reading the built binary's + # DT_NEEDED — so the bound tracks whatever FFmpeg the builder linked against, with nothing to + # hand-maintain across the next bump. pacman then REFUSES the ffmpeg upgrade instead of bricking + # the install, and the canary rebuilt on an ffmpeg-9 image installs cleanly on an ffmpeg-9 box. + # A hand-written `ffmpeg<2:9` would have to be edited (and would go stale) on every bump; this + # does not. NOT bundling FFmpeg the way the .deb does (BUNDLE_FFMPEG=1): that exists because + # Ubuntu 24.04 LTS is frozen on FFmpeg 6.1 and can never satisfy the dep, whereas rolling Arch + # always ships a current FFmpeg — and vendoring a second copy of a system library is against + # Arch packaging doctrine (and would not survive AUR review). + # All seven are listed even though --as-needed currently drops libavformat/libswresample from the + # link: an unlinked soname is left bare by makepkg and is satisfied by any ffmpeg, so listing it + # costs nothing, while a future link picking one up gets the version bound automatically. depends=('ffmpeg' 'pipewire' 'wireplumber' 'opus' 'libei' - 'mesa' 'libglvnd' 'libxkbcommon' 'wayland') + 'mesa' 'libglvnd' 'libxkbcommon' 'wayland' + 'libavcodec.so' 'libavutil.so' 'libavfilter.so' 'libavdevice.so' + 'libavformat.so' 'libswscale.so' 'libswresample.so') optdepends=('pipewire-pulse: PulseAudio-API audio from games/apps (real `pulseaudio` also works)' 'nvidia-utils: NVENC hardware encode + GPU EGL/CUDA zero-copy (REQUIRED to encode on NVIDIA)' 'gamescope: per-session nested compositor backend (no desktop login needed) — needs >=3.16.22' From deeb8b67000e4faa4939f736a3a21004b47e249a Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 8 Aug 2026 02:35:20 +0200 Subject: [PATCH 6/6] feat(pf-encode): build against FFmpeg 9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ffmpeg-next 8.1.0 could not accept FFmpeg 9 at all: ffmpeg-sys-next's version probe covered avcodec majors 56..62 (the range is exclusive of its end), so libavcodec 63 fell outside what it knew how to bind. 9.0.0 widens that to 56..63, which is what actually unblocks Arch. Bump both pins — the unconditional Linux dep and the optional Windows amf-qsv one — and the lock with them. No API drift to fix. The crate major is a CEILING, not a target: one source tree still spans FFmpeg 7.x/libavcodec 61, 8.x/62 and 9.x/63 via per-version cfgs, and every wrapper symbol the NVENC-libav, VAAPI and amf-qsv backends name survives 8.1.0 -> 9.0.0 unchanged. The three hand-written #[repr(C)] hwcontext mirrors are the parts no compiler checks, so they were re-read against the real headers rather than trusted: AVCUDADeviceContext and AVD3D11VAFramesContext are byte-identical across 7.1/8/9, and AVD3D11VADeviceContext gained two trailing UINTs in 8 that 7.1 lacks — which is why that mirror deliberately stops at the common prefix, and why its assertions now say what they do and do not buy you. They pin our layout, not libav's; a green build is not evidence. The CI image is the step that makes this reach users. arch.yml deliberately runs no -Syu ("the image's snapshot IS the build environment"), so the builder stayed frozen on ffmpeg 8 no matter what Arch shipped, and a canary built from that snapshot could not satisfy the soname dep the PKGBUILD now derives. Re-keying ci/ rebuilds it against ffmpeg 9. Ubuntu and Windows deliberately stay put: the noble .deb bundles its own FFmpeg 8 behind an rpath and strips the libav sonames from its Depends, and Windows bundles BtbN DLLs into the signed installer — neither is exposed to the break, BtbN publishes no FFmpeg 9 build, and moving either would re-qualify an encode stack to buy nothing. Verified end to end on 192.168.1.21 (CachyOS, system ffmpeg 2:9.0-5, RTX 5070 Ti): host builds clean and links libavcodec.so.63/libavutil.so.61/libavfilter.so.12/libswscale.so.10 with no unresolved sonames; the ffmpeg-8 compat shim is gone and the service runs with NRestarts=0 and answers 401 on :47990; pf-encode's 67 tests pass; and a live synthetic encode drives real NVENC hardware through FFmpeg 9's libavcodec to a decodable 1080p HEVC stream (180/180 frames, FEC loopback 0 mismatches) with libavcodec.so.63 and libnvidia-encode both mapped into the encoding process. --- Cargo.lock | 16 +++++++------- THIRD-PARTY-NOTICES.txt | 16 +++++++------- ci/arch-ci.Dockerfile | 11 +++++++++- ci/rust-ci-noble.Dockerfile | 8 +++++++ ci/rust-ci.Dockerfile | 4 +++- .../src/main/assets/THIRD-PARTY-NOTICES.txt | 8 +++---- .../Resources/THIRD-PARTY-NOTICES.txt | 8 +++---- clients/linux/THIRD-PARTY-NOTICES.txt | 10 ++++----- clients/windows/THIRD-PARTY-NOTICES.txt | 10 ++++----- crates/pf-encode/Cargo.toml | 10 ++++++--- crates/pf-encode/src/enc/linux/mod.rs | 8 +++++-- .../pf-encode/src/enc/windows/ffmpeg_win.rs | 22 +++++++++++++++---- .../ci/provision-windows-punktfunk-extras.ps1 | 11 ++++++++++ 13 files changed, 97 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8401cb07..798a1a54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -647,9 +647,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.65" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "9066c49992464636f92905fa096ec58baaa4d57ec19a5c096c68d3e25ef3d136" dependencies = [ "find-msvc-tools", "jobserver", @@ -1290,9 +1290,9 @@ dependencies = [ [[package]] name = "ffmpeg-next" -version = "8.1.0" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c4bd5ab1ac61f29c634df1175d350ded29cf74c3c6d4f7030431a5ae3c7d5d" +checksum = "6380599799e175191eb7ffe82c97f36a2a90a36cbc54c738a903e5287d7f516a" dependencies = [ "bitflags 2.13.0", "ffmpeg-sys-next", @@ -1301,9 +1301,9 @@ dependencies = [ [[package]] name = "ffmpeg-sys-next" -version = "8.1.0" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a314bc0e022a33a99567ed4bd2576bd58ffd8fcff7891c29194cfecc26a62547" +checksum = "9b939bf79dd5949412a4b81cfe21a07f48ea21b47fcbb5f57816c8c2de5ae30b" dependencies = [ "bindgen", "cc", @@ -1341,9 +1341,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" [[package]] name = "fixedbitset" diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index 94ce4664..fd6c69fd 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -83,7 +83,7 @@ MANIFEST (crate version — SPDX license — source) cast 0.3.0 — MIT OR Apache-2.0 — https://github.com/japaric/cast.rs cbc 0.1.2 — MIT OR Apache-2.0 — https://github.com/RustCrypto/block-modes cbindgen 0.29.4 — MPL-2.0 — https://github.com/mozilla/cbindgen - cc 1.2.65 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + cc 1.4.1 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs cesu8 1.1.0 — Apache-2.0/MIT — https://github.com/emk/cesu8-rs cexpr 0.6.0 — Apache-2.0/MIT — https://github.com/jethrogb/rust-cexpr cfg-expr 0.20.8 — MIT OR Apache-2.0 — https://github.com/EmbarkStudios/cfg-expr @@ -148,12 +148,12 @@ MANIFEST (crate version — SPDX license — source) fastbloom 0.14.1 — MIT OR Apache-2.0 — https://github.com/tomtomwombat/fastbloom/ fastrand 2.4.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/fastrand fdeflate 0.3.7 — MIT OR Apache-2.0 — https://github.com/image-rs/fdeflate - ffmpeg-next 8.1.0 — WTFPL — https://github.com/zmwangx/rust-ffmpeg - ffmpeg-sys-next 8.1.0 — WTFPL — https://github.com/zmwangx/rust-ffmpeg-sys + ffmpeg-next 9.0.0 — WTFPL — https://github.com/zmwangx/rust-ffmpeg + ffmpeg-sys-next 9.0.0 — WTFPL — https://github.com/zmwangx/rust-ffmpeg-sys fiat-crypto 0.2.9 — MIT OR Apache-2.0 OR BSD-1-Clause — https://github.com/mit-plv/fiat-crypto field-offset 0.3.6 — MIT OR Apache-2.0 — https://github.com/Diggsey/rust-field-offset filetime 0.2.29 — MIT/Apache-2.0 — https://github.com/alexcrichton/filetime - find-msvc-tools 0.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + find-msvc-tools 0.1.10 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs fixedbitset 0.5.7 — MIT OR Apache-2.0 — https://github.com/petgraph/fixedbitset flate2 1.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/flate2-rs flume 0.12.0 — Apache-2.0/MIT — https://github.com/zesterer/flume @@ -628,7 +628,7 @@ Crates whose package did not embed a license file (SPDX + source only) atomig-macro 0.4.0 — MIT/Apache-2.0 — https://github.com/LukasKalbertodt/atomig/ cookie-factory 0.3.3 — MIT — https://github.com/rust-bakery/cookie-factory defmt-parser 1.0.0 — MIT OR Apache-2.0 — https://github.com/knurling-rs/defmt - ffmpeg-sys-next 8.1.0 — WTFPL — https://github.com/zmwangx/rust-ffmpeg-sys + ffmpeg-sys-next 9.0.0 — WTFPL — https://github.com/zmwangx/rust-ffmpeg-sys jni-sys-macros 0.4.1 — MIT OR Apache-2.0 — https://github.com/jni-rs/jni-sys ndk 0.9.0 — MIT OR Apache-2.0 — https://github.com/rust-mobile/ndk ndk-sys 0.6.0+11769913 — MIT OR Apache-2.0 — https://github.com/rust-mobile/ndk @@ -2266,7 +2266,7 @@ SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: asn1-rs 0.6.2, asn1-rs-derive 0.5.1, assert_matches 1.5.0, async-channel 2.5.0, async-executor 1.14.0, async-io 2.6.0, async-lock 3.4.2, async-process 2.5.0, async-recursion 1.1.1, async-signal 0.2.14, async-task 4.7.1, atomic-waker 1.1.2, autocfg 1.5.1, base64 0.22.1, bitflags 1.3.2, bitflags 2.13.0, blocking 1.6.2, bumpalo 3.20.3, cast 0.3.0, cc 1.2.65, cexpr 0.6.0, cfg-if 1.0.4, cmake 0.1.58, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, der-parser 9.0.0, displaydoc 0.2.6, either 1.16.0, equivalent 1.0.2, errno 0.3.14, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, filetime 0.2.29, find-msvc-tools 0.1.9, fixedbitset 0.5.7, flate2 1.1.9, fnv 1.0.7, form_urlencoded 1.2.2, fs-err 3.3.0, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glob 0.3.3, hashbrown 0.16.1, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, httparse 1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.0, itertools 0.10.5, itertools 0.13.0, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, khronos-egl 6.0.0, lazy_static 1.5.0, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, mime 0.3.17, nasm-rs 0.3.2, num-bigint 0.4.6, num-bigint-dig 0.8.6, num-derive 0.4.2, num-integer 0.1.46, num-iter 0.1.45, num-traits 0.2.19, num_cpus 1.17.0, oid-registry 0.7.1, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5, pkg-config 0.3.33, png 0.18.1, polling 3.11.0, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, roxmltree 0.21.1, rsa 0.9.10, rustc_version 0.4.1, rusticata-macros 4.1.0, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, stable_deref_trait 1.2.1, system-deps 7.0.8, tar 0.4.46, tempfile 3.27.0, thread_local 1.1.9, tinytemplate 1.2.1, unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.23.4, vcpkg 0.2.15, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, weezl 0.1.12, wit-bindgen 0.57.1, x509-parser 0.16.0, xattr 1.6.1 +The following license (LICENSE-APACHE) applies to: asn1-rs 0.6.2, asn1-rs-derive 0.5.1, assert_matches 1.5.0, async-channel 2.5.0, async-executor 1.14.0, async-io 2.6.0, async-lock 3.4.2, async-process 2.5.0, async-recursion 1.1.1, async-signal 0.2.14, async-task 4.7.1, atomic-waker 1.1.2, autocfg 1.5.1, base64 0.22.1, bitflags 1.3.2, bitflags 2.13.0, blocking 1.6.2, bumpalo 3.20.3, cast 0.3.0, cc 1.4.1, cexpr 0.6.0, cfg-if 1.0.4, cmake 0.1.58, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, der-parser 9.0.0, displaydoc 0.2.6, either 1.16.0, equivalent 1.0.2, errno 0.3.14, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, filetime 0.2.29, find-msvc-tools 0.1.10, fixedbitset 0.5.7, flate2 1.1.9, fnv 1.0.7, form_urlencoded 1.2.2, fs-err 3.3.0, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glob 0.3.3, hashbrown 0.16.1, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, httparse 1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.0, itertools 0.10.5, itertools 0.13.0, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, khronos-egl 6.0.0, lazy_static 1.5.0, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, mime 0.3.17, nasm-rs 0.3.2, num-bigint 0.4.6, num-bigint-dig 0.8.6, num-derive 0.4.2, num-integer 0.1.46, num-iter 0.1.45, num-traits 0.2.19, num_cpus 1.17.0, oid-registry 0.7.1, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, piper 0.2.5, pkg-config 0.3.33, png 0.18.1, polling 3.11.0, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, roxmltree 0.21.1, rsa 0.9.10, rustc_version 0.4.1, rusticata-macros 4.1.0, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, stable_deref_trait 1.2.1, system-deps 7.0.8, tar 0.4.46, tempfile 3.27.0, thread_local 1.1.9, tinytemplate 1.2.1, unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.23.4, vcpkg 0.2.15, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, weezl 0.1.12, wit-bindgen 0.57.1, x509-parser 0.16.0, xattr 1.6.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -4011,7 +4011,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, filetime 0.2.29, find-msvc-tools 0.1.9, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 +The following license (LICENSE-MIT) applies to: cc 1.4.1, cfg-if 1.0.4, cmake 0.1.58, filetime 0.2.29, find-msvc-tools 0.1.10, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 ---------------------------------------------------------------------------- Copyright (c) 2014 Alex Crichton @@ -6183,7 +6183,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE) applies to: ffmpeg-next 8.1.0 +The following license (LICENSE) applies to: ffmpeg-next 9.0.0 ---------------------------------------------------------------------------- DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 diff --git a/ci/arch-ci.Dockerfile b/ci/arch-ci.Dockerfile index 5eb243ae..ec71a655 100644 --- a/ci/arch-ci.Dockerfile +++ b/ci/arch-ci.Dockerfile @@ -9,7 +9,16 @@ # from the last image rebuild instead of a fresh -Syu per run. That is the same staleness # the gamescope cache already embraces ("a stale binary against newer system libs is the # same risk the distro's own package carries between rebuilds"), and any ci/ edit — or -# bumping the date in this line (refreshed: 2026-07-29) — re-keys and re-snapshots it. +# bumping the date in this line (refreshed: 2026-08-08) — re-keys and re-snapshots it. +# +# ⚠ That staleness has a sharp edge, and 2026-08-08 is why the date above moved: this snapshot is +# what decides which FFmpeg the HOST links, and arch.yml deliberately runs no -Syu, so the builder +# stayed frozen on ffmpeg 8 (libavcodec 62) even after Arch shipped 2:9.0-5 (libavcodec 63) to +# every user. A canary built from the old snapshot therefore CANNOT satisfy the soname dep that +# packaging/arch/PKGBUILD now derives from the link (libavcodec.so=62-64 against a box that has +# 63-64), so it would simply refuse to install rather than start. Re-keying this image is the step +# that makes the ffmpeg-9 bump actually reach the package — a Cargo.toml bump alone does nothing +# here. Whenever Arch moves to an FFmpeg major, bump the date in the same commit. FROM docker.io/library/archlinux:base-devel # One transaction: the main build/runtime deps (first list) + the gamescope companion's diff --git a/ci/rust-ci-noble.Dockerfile b/ci/rust-ci-noble.Dockerfile index 55aa4f6a..1b3d0aa1 100644 --- a/ci/rust-ci-noble.Dockerfile +++ b/ci/rust-ci-noble.Dockerfile @@ -45,6 +45,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # Sourced from the official FFmpeg GitHub mirror by release tag, NOT ffmpeg.org: the CI build network # can't reach ffmpeg.org (curl times out) but reaches github.com fine. The `nX.Y` tag pins the version # (n8.0 -> libavcodec 62); bump it to move FFmpeg. Immutable-tag clone, so no separate checksum needed. +# +# STAYING ON 8.0 THROUGH THE 2026-08-08 FFmpeg-9 BUMP IS DELIBERATE. `ffmpeg-next` moved to 9, but a +# crate major is a CEILING (ffmpeg-sys-next 9 spans libavcodec 56..63), so an 8.0 tree still compiles +# — and this .deb is the one package with NO exposure to the soname break that motivated the bump: it +# BUNDLES these libs into /usr/lib/punktfunk-host behind an rpath and strips the libav* sonames from +# its Depends, so nothing the user's apt does can move them underneath it. Bumping this tag would +# re-qualify the encode stack for every Ubuntu user and buy none of them anything, so it is its own +# change — and it drags NVHDR_TAG and the soname assertion below along with it. ARG FFMPEG_TAG=n8.0 # nv-codec-headers must MATCH the FFmpeg version: its `master` is NVENC SDK 13, which renamed # NV_ENC_CLOCK_TIMESTAMP_SET.countingType -> countingTypeLSB and won't compile against FFmpeg 8.0's diff --git a/ci/rust-ci.Dockerfile b/ci/rust-ci.Dockerfile index a4114ccc..9ca327ca 100644 --- a/ci/rust-ci.Dockerfile +++ b/ci/rust-ci.Dockerfile @@ -13,7 +13,9 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y --no-install-recommends \ # toolchain + bindgen; nodejs runs the JS actions (checkout/cache); unzip is for the bun installer build-essential clang libclang-dev pkg-config cmake git curl ca-certificates nodejs unzip \ - # ffmpeg-next 8 (system FFmpeg 8 / libavcodec 62 on 26.04) + # ffmpeg-next 9, built against whatever libav* 26.04 ships (FFmpeg 8 / libavcodec 62 today). + # The crate major is a CEILING — ffmpeg-sys-next 9 spans libavcodec 56..63 — so this image does + # not need to move in lockstep with Arch's FFmpeg 9; it just links what the distro has. libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavfilter-dev \ libavdevice-dev \ # capture / audio / display stacks (+xkbcommon for the wlr input backend) diff --git a/clients/android/app/src/main/assets/THIRD-PARTY-NOTICES.txt b/clients/android/app/src/main/assets/THIRD-PARTY-NOTICES.txt index 7ec104a4..08bcfe1f 100644 --- a/clients/android/app/src/main/assets/THIRD-PARTY-NOTICES.txt +++ b/clients/android/app/src/main/assets/THIRD-PARTY-NOTICES.txt @@ -49,7 +49,7 @@ MANIFEST (crate version — SPDX license — source) bytes 1.12.0 — MIT — https://github.com/tokio-rs/bytes cast 0.3.0 — MIT OR Apache-2.0 — https://github.com/japaric/cast.rs cbindgen 0.29.4 — MPL-2.0 — https://github.com/mozilla/cbindgen - cc 1.2.65 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + cc 1.4.1 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs cesu8 1.1.0 — Apache-2.0/MIT — https://github.com/emk/cesu8-rs cfg-if 1.0.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/cfg-if cfg_aliases 0.2.1 — MIT — https://github.com/katharostech/cfg_aliases @@ -88,7 +88,7 @@ MANIFEST (crate version — SPDX license — source) fastbloom 0.14.1 — MIT OR Apache-2.0 — https://github.com/tomtomwombat/fastbloom/ fastrand 2.4.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/fastrand fiat-crypto 0.2.9 — MIT OR Apache-2.0 OR BSD-1-Clause — https://github.com/mit-plv/fiat-crypto - find-msvc-tools 0.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + find-msvc-tools 0.1.10 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs fixedbitset 0.5.7 — MIT OR Apache-2.0 — https://github.com/petgraph/fixedbitset flume 0.12.0 — Apache-2.0/MIT — https://github.com/zesterer/flume fnv 1.0.7 — Apache-2.0 / MIT — https://github.com/servo/rust-fnv @@ -1390,7 +1390,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: autocfg 1.5.1, base64 0.22.1, bitflags 2.13.0, bumpalo 3.20.3, cast 0.3.0, cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, either 1.16.0, equivalent 1.0.2, errno 0.3.14, fastrand 2.4.1, find-msvc-tools 0.1.9, fixedbitset 0.5.7, fnv 1.0.7, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, indexmap 2.14.0, itertools 0.10.5, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, parking_lot 0.12.5, parking_lot_core 0.9.12, pkg-config 0.3.33, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, rustc_version 0.4.1, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, tempfile 3.27.0, tinytemplate 1.2.1, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, wit-bindgen 0.57.1 +The following license (LICENSE-APACHE) applies to: autocfg 1.5.1, base64 0.22.1, bitflags 2.13.0, bumpalo 3.20.3, cast 0.3.0, cc 1.4.1, cfg-if 1.0.4, cmake 0.1.58, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, either 1.16.0, equivalent 1.0.2, errno 0.3.14, fastrand 2.4.1, find-msvc-tools 0.1.10, fixedbitset 0.5.7, fnv 1.0.7, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, indexmap 2.14.0, itertools 0.10.5, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, parking_lot 0.12.5, parking_lot_core 0.9.12, pkg-config 0.3.33, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, rustc_version 0.4.1, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, tempfile 3.27.0, tinytemplate 1.2.1, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, wit-bindgen 0.57.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -2435,7 +2435,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, find-msvc-tools 0.1.9, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 +The following license (LICENSE-MIT) applies to: cc 1.4.1, cfg-if 1.0.4, cmake 0.1.58, find-msvc-tools 0.1.10, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 ---------------------------------------------------------------------------- Copyright (c) 2014 Alex Crichton diff --git a/clients/apple/Sources/PunktfunkKit/Resources/THIRD-PARTY-NOTICES.txt b/clients/apple/Sources/PunktfunkKit/Resources/THIRD-PARTY-NOTICES.txt index 5646bef3..50cc3bf3 100644 --- a/clients/apple/Sources/PunktfunkKit/Resources/THIRD-PARTY-NOTICES.txt +++ b/clients/apple/Sources/PunktfunkKit/Resources/THIRD-PARTY-NOTICES.txt @@ -47,7 +47,7 @@ MANIFEST (crate version — SPDX license — source) bytes 1.12.0 — MIT — https://github.com/tokio-rs/bytes cast 0.3.0 — MIT OR Apache-2.0 — https://github.com/japaric/cast.rs cbindgen 0.29.4 — MPL-2.0 — https://github.com/mozilla/cbindgen - cc 1.2.65 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + cc 1.4.1 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs cesu8 1.1.0 — Apache-2.0/MIT — https://github.com/emk/cesu8-rs cfg-if 1.0.4 — MIT OR Apache-2.0 — https://github.com/rust-lang/cfg-if cfg_aliases 0.2.1 — MIT — https://github.com/katharostech/cfg_aliases @@ -85,7 +85,7 @@ MANIFEST (crate version — SPDX license — source) fastbloom 0.14.1 — MIT OR Apache-2.0 — https://github.com/tomtomwombat/fastbloom/ fastrand 2.4.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/fastrand fiat-crypto 0.2.9 — MIT OR Apache-2.0 OR BSD-1-Clause — https://github.com/mit-plv/fiat-crypto - find-msvc-tools 0.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + find-msvc-tools 0.1.10 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs fixedbitset 0.5.7 — MIT OR Apache-2.0 — https://github.com/petgraph/fixedbitset fnv 1.0.7 — Apache-2.0 / MIT — https://github.com/servo/rust-fnv foldhash 0.2.0 — Zlib — https://github.com/orlp/foldhash @@ -908,7 +908,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: autocfg 1.5.1, base64 0.22.1, bitflags 2.13.0, bumpalo 3.20.3, cast 0.3.0, cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, either 1.16.0, equivalent 1.0.2, errno 0.3.14, fastrand 2.4.1, find-msvc-tools 0.1.9, fixedbitset 0.5.7, fnv 1.0.7, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, indexmap 2.14.0, itertools 0.10.5, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, parking_lot 0.12.5, parking_lot_core 0.9.12, pkg-config 0.3.33, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, rustc_version 0.4.1, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, tempfile 3.27.0, tinytemplate 1.2.1, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, wit-bindgen 0.57.1 +The following license (LICENSE-APACHE) applies to: autocfg 1.5.1, base64 0.22.1, bitflags 2.13.0, bumpalo 3.20.3, cast 0.3.0, cc 1.4.1, cfg-if 1.0.4, cmake 0.1.58, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, either 1.16.0, equivalent 1.0.2, errno 0.3.14, fastrand 2.4.1, find-msvc-tools 0.1.10, fixedbitset 0.5.7, fnv 1.0.7, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, indexmap 2.14.0, itertools 0.10.5, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, parking_lot 0.12.5, parking_lot_core 0.9.12, pkg-config 0.3.33, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, rustc_version 0.4.1, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, tempfile 3.27.0, tinytemplate 1.2.1, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, wit-bindgen 0.57.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -1953,7 +1953,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, find-msvc-tools 0.1.9, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 +The following license (LICENSE-MIT) applies to: cc 1.4.1, cfg-if 1.0.4, cmake 0.1.58, find-msvc-tools 0.1.10, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 ---------------------------------------------------------------------------- Copyright (c) 2014 Alex Crichton diff --git a/clients/linux/THIRD-PARTY-NOTICES.txt b/clients/linux/THIRD-PARTY-NOTICES.txt index 280c1365..bcf1b24f 100644 --- a/clients/linux/THIRD-PARTY-NOTICES.txt +++ b/clients/linux/THIRD-PARTY-NOTICES.txt @@ -1,7 +1,7 @@ THIRD-PARTY SOFTWARE NOTICES ============================================================================ -punktfunk (https://git.unom.io/unom/punktfunk) is licensed under MIT OR Apache-2.0. +Punktfunk (https://git.unom.io/unom/punktfunk) is licensed under MIT OR Apache-2.0. The binaries it ships statically/dynamically link the third-party Rust crates listed below. Each is distributed under its own permissive license; the full license texts follow the manifest. This file is generated by scripts/gen-third-party-notices.py @@ -62,7 +62,7 @@ MANIFEST (crate version — SPDX license — source) cairo-sys-rs 0.22.0 — MIT — https://github.com/gtk-rs/gtk-rs-core cast 0.3.0 — MIT OR Apache-2.0 — https://github.com/japaric/cast.rs cbindgen 0.29.4 — MPL-2.0 — https://github.com/mozilla/cbindgen - cc 1.2.65 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + cc 1.4.1 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs cesu8 1.1.0 — Apache-2.0/MIT — https://github.com/emk/cesu8-rs cexpr 0.6.0 — Apache-2.0/MIT — https://github.com/jethrogb/rust-cexpr cfg-expr 0.20.8 — MIT OR Apache-2.0 — https://github.com/EmbarkStudios/cfg-expr @@ -117,7 +117,7 @@ MANIFEST (crate version — SPDX license — source) fiat-crypto 0.2.9 — MIT OR Apache-2.0 OR BSD-1-Clause — https://github.com/mit-plv/fiat-crypto field-offset 0.3.6 — MIT OR Apache-2.0 — https://github.com/Diggsey/rust-field-offset filetime 0.2.29 — MIT/Apache-2.0 — https://github.com/alexcrichton/filetime - find-msvc-tools 0.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + find-msvc-tools 0.1.10 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs fixedbitset 0.5.7 — MIT OR Apache-2.0 — https://github.com/petgraph/fixedbitset flate2 1.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/flate2-rs flume 0.12.0 — Apache-2.0/MIT — https://github.com/zesterer/flume @@ -1625,7 +1625,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: assert_matches 1.5.0, async-channel 2.5.0, autocfg 1.5.1, base64 0.22.1, bitflags 1.3.2, bitflags 2.13.0, bumpalo 3.20.3, cast 0.3.0, cc 1.2.65, cexpr 0.6.0, cfg-if 1.0.4, cmake 0.1.58, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, displaydoc 0.2.6, either 1.16.0, equivalent 1.0.2, errno 0.3.14, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, filetime 0.2.29, find-msvc-tools 0.1.9, fixedbitset 0.5.7, flate2 1.1.9, fnv 1.0.7, form_urlencoded 1.2.2, glob 0.3.3, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.0, itertools 0.10.5, itertools 0.13.0, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, lazy_static 1.5.0, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, nasm-rs 0.3.2, num-integer 0.1.46, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, pkg-config 0.3.33, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, rustc_version 0.4.1, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, stable_deref_trait 1.2.1, system-deps 7.0.8, tar 0.4.46, tempfile 3.27.0, thread_local 1.1.9, tinytemplate 1.2.1, unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, vcpkg 0.2.15, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, wit-bindgen 0.57.1, xattr 1.6.1 +The following license (LICENSE-APACHE) applies to: assert_matches 1.5.0, async-channel 2.5.0, autocfg 1.5.1, base64 0.22.1, bitflags 1.3.2, bitflags 2.13.0, bumpalo 3.20.3, cast 0.3.0, cc 1.4.1, cexpr 0.6.0, cfg-if 1.0.4, cmake 0.1.58, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, displaydoc 0.2.6, either 1.16.0, equivalent 1.0.2, errno 0.3.14, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, filetime 0.2.29, find-msvc-tools 0.1.10, fixedbitset 0.5.7, flate2 1.1.9, fnv 1.0.7, form_urlencoded 1.2.2, glob 0.3.3, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.0, itertools 0.10.5, itertools 0.13.0, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, lazy_static 1.5.0, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, nasm-rs 0.3.2, num-integer 0.1.46, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, pkg-config 0.3.33, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, rustc_version 0.4.1, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, stable_deref_trait 1.2.1, system-deps 7.0.8, tar 0.4.46, tempfile 3.27.0, thread_local 1.1.9, tinytemplate 1.2.1, unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, vcpkg 0.2.15, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, wit-bindgen 0.57.1, xattr 1.6.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -2921,7 +2921,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, filetime 0.2.29, find-msvc-tools 0.1.9, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 +The following license (LICENSE-MIT) applies to: cc 1.4.1, cfg-if 1.0.4, cmake 0.1.58, filetime 0.2.29, find-msvc-tools 0.1.10, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 ---------------------------------------------------------------------------- Copyright (c) 2014 Alex Crichton diff --git a/clients/windows/THIRD-PARTY-NOTICES.txt b/clients/windows/THIRD-PARTY-NOTICES.txt index 3f7c7a5e..e131eb09 100644 --- a/clients/windows/THIRD-PARTY-NOTICES.txt +++ b/clients/windows/THIRD-PARTY-NOTICES.txt @@ -1,7 +1,7 @@ THIRD-PARTY SOFTWARE NOTICES ============================================================================ -punktfunk (https://git.unom.io/unom/punktfunk) is licensed under MIT OR Apache-2.0. +Punktfunk (https://git.unom.io/unom/punktfunk) is licensed under MIT OR Apache-2.0. The binaries it ships statically/dynamically link the third-party Rust crates listed below. Each is distributed under its own permissive license; the full license texts follow the manifest. This file is generated by scripts/gen-third-party-notices.py @@ -60,7 +60,7 @@ MANIFEST (crate version — SPDX license — source) bytes 1.12.0 — MIT — https://github.com/tokio-rs/bytes cast 0.3.0 — MIT OR Apache-2.0 — https://github.com/japaric/cast.rs cbindgen 0.29.4 — MPL-2.0 — https://github.com/mozilla/cbindgen - cc 1.2.65 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + cc 1.4.1 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs cesu8 1.1.0 — Apache-2.0/MIT — https://github.com/emk/cesu8-rs cexpr 0.6.0 — Apache-2.0/MIT — https://github.com/jethrogb/rust-cexpr cfg-expr 0.20.8 — MIT OR Apache-2.0 — https://github.com/EmbarkStudios/cfg-expr @@ -114,7 +114,7 @@ MANIFEST (crate version — SPDX license — source) fastrand 2.4.1 — Apache-2.0 OR MIT — https://github.com/smol-rs/fastrand fiat-crypto 0.2.9 — MIT OR Apache-2.0 OR BSD-1-Clause — https://github.com/mit-plv/fiat-crypto filetime 0.2.29 — MIT/Apache-2.0 — https://github.com/alexcrichton/filetime - find-msvc-tools 0.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs + find-msvc-tools 0.1.10 — MIT OR Apache-2.0 — https://github.com/rust-lang/cc-rs fixedbitset 0.5.7 — MIT OR Apache-2.0 — https://github.com/petgraph/fixedbitset flate2 1.1.9 — MIT OR Apache-2.0 — https://github.com/rust-lang/flate2-rs flume 0.12.0 — Apache-2.0/MIT — https://github.com/zesterer/flume @@ -1598,7 +1598,7 @@ DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- -The following license (LICENSE-APACHE) applies to: assert_matches 1.5.0, async-channel 2.5.0, autocfg 1.5.1, base64 0.22.1, bitflags 1.3.2, bitflags 2.13.0, bumpalo 3.20.3, cast 0.3.0, cc 1.2.65, cexpr 0.6.0, cfg-if 1.0.4, cmake 0.1.58, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, displaydoc 0.2.6, either 1.16.0, equivalent 1.0.2, errno 0.3.14, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, filetime 0.2.29, find-msvc-tools 0.1.9, fixedbitset 0.5.7, flate2 1.1.9, fnv 1.0.7, form_urlencoded 1.2.2, glob 0.3.3, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.0, itertools 0.10.5, itertools 0.13.0, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, lazy_static 1.5.0, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, nasm-rs 0.3.2, num-integer 0.1.46, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, pkg-config 0.3.33, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, rustc_version 0.4.1, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, stable_deref_trait 1.2.1, system-deps 7.0.8, tar 0.4.46, tempfile 3.27.0, thread_local 1.1.9, tinytemplate 1.2.1, unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, vcpkg 0.2.15, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, wit-bindgen 0.57.1, xattr 1.6.1 +The following license (LICENSE-APACHE) applies to: assert_matches 1.5.0, async-channel 2.5.0, autocfg 1.5.1, base64 0.22.1, bitflags 1.3.2, bitflags 2.13.0, bumpalo 3.20.3, cast 0.3.0, cc 1.4.1, cexpr 0.6.0, cfg-if 1.0.4, cmake 0.1.58, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation-sys 0.8.7, criterion 0.5.1, criterion-plot 0.5.0, crossbeam-deque 0.8.6, crossbeam-epoch 0.9.20, crossbeam-utils 0.8.21, curve25519-dalek-derive 0.1.1, displaydoc 0.2.6, either 1.16.0, equivalent 1.0.2, errno 0.3.14, event-listener 5.4.1, event-listener-strategy 0.5.4, fastrand 2.4.1, filetime 0.2.29, find-msvc-tools 0.1.10, fixedbitset 0.5.7, flate2 1.1.9, fnv 1.0.7, form_urlencoded 1.2.2, glob 0.3.3, hashbrown 0.17.1, heck 0.5.0, hermit-abi 0.5.2, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.0, itertools 0.10.5, itertools 0.13.0, jni 0.21.1, jobserver 0.1.34, js-sys 0.3.103, lazy_static 1.5.0, linux-raw-sys 0.12.1, lock_api 0.4.14, log 0.4.33, nasm-rs 0.3.2, num-integer 0.1.46, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, opus 0.3.1, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, pkg-config 0.3.33, proptest 1.11.0, rayon 1.12.0, rayon-core 1.13.0, regex 1.12.4, regex-automata 0.4.14, regex-syntax 0.8.11, rustc_version 0.4.1, rustix 1.1.4, rustls 0.23.41, rustls-native-certs 0.8.4, rusty-fork 0.3.1, scopeguard 1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, smallvec 1.15.2, socket2 0.6.4, stable_deref_trait 1.2.1, system-deps 7.0.8, tar 0.4.46, tempfile 3.27.0, thread_local 1.1.9, tinytemplate 1.2.1, unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, vcpkg 0.2.15, version_check 0.9.5, wait-timeout 0.2.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126, wit-bindgen 0.57.1, xattr 1.6.1 ---------------------------------------------------------------------------- Apache License Version 2.0, January 2004 @@ -2853,7 +2853,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------------------------- -The following license (LICENSE-MIT) applies to: cc 1.2.65, cfg-if 1.0.4, cmake 0.1.58, filetime 0.2.29, find-msvc-tools 0.1.9, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 +The following license (LICENSE-MIT) applies to: cc 1.4.1, cfg-if 1.0.4, cmake 0.1.58, filetime 0.2.29, find-msvc-tools 0.1.10, jobserver 0.1.34, js-sys 0.3.103, openssl-probe 0.2.1, pkg-config 0.3.33, socket2 0.6.4, wait-timeout 0.2.1, wasm-bindgen 0.2.126, wasm-bindgen-macro 0.2.126, wasm-bindgen-macro-support 0.2.126, wasm-bindgen-shared 0.2.126 ---------------------------------------------------------------------------- Copyright (c) 2014 Alex Crichton diff --git a/crates/pf-encode/Cargo.toml b/crates/pf-encode/Cargo.toml index 82818060..6ffed5c6 100644 --- a/crates/pf-encode/Cargo.toml +++ b/crates/pf-encode/Cargo.toml @@ -35,8 +35,12 @@ pf-capture = { path = "../pf-capture" } openh264 = "0.9" [target.'cfg(target_os = "linux")'.dependencies] -# libavcodec (NVENC libav + VAAPI backends). `ffmpeg-sys-next` auto-detects the FFmpeg version. -ffmpeg-next = "8" +# libavcodec (NVENC libav + VAAPI backends). `ffmpeg-sys-next` auto-detects the FFmpeg version, so +# this pin tracks the crate's own major (which shadows FFmpeg's): 9 = FFmpeg 9 (libavcodec 63, +# libavutil 61). Arch shipped FFmpeg 9 on 2026-08-08 and every soname moved with it; the packaged +# host must be BUILT against the FFmpeg it will run on, and packaging/arch/PKGBUILD now derives a +# soname dep from that link so pacman can no longer walk an install across the break. +ffmpeg-next = "9" libc = "0.2" # Vulkan bindings for the raw Vulkan-Video encode + PyroWave compute backends (feature-gated below; # the dep stays unconditional to mirror the host's Linux target — unused-but-declared is harmless). @@ -53,7 +57,7 @@ pyrowave-sys = { path = "../pyrowave-sys", optional = true } # NVENC (direct SDK, D3D11 input) + the shared D3D11/DXGI vocabulary via pf-frame. nvidia-video-codec-sdk = { version = "0.4", features = ["ci-check"], optional = true } # AMD (AMF) + Intel (QSV) hardware encode via libavcodec (behind `amf-qsv`; link-imports FFmpeg). -ffmpeg-next = { version = "8", optional = true } +ffmpeg-next = { version = "9", optional = true } # `libnvidia-encode`/`nvEncodeAPI64.dll` resolved at runtime; the NVENC status→cause table dlopen. libloading = "0.8" # Native Intel QSV (VPL): vendored static MIT dispatcher + bindgen'd C API, only under `qsv`. diff --git a/crates/pf-encode/src/enc/linux/mod.rs b/crates/pf-encode/src/enc/linux/mod.rs index ab783e9d..192a994c 100644 --- a/crates/pf-encode/src/enc/linux/mod.rs +++ b/crates/pf-encode/src/enc/linux/mod.rs @@ -1,6 +1,10 @@ //! NVENC encoder via `ffmpeg-next` (binds the system FFmpeg — `ffmpeg-sys-next` auto-detects the -//! installed version, so this builds against FFmpeg 7.x/libavcodec 61 *or* 8.x/libavcodec 62; -//! validated live on Ubuntu 26.04 (FFmpeg 8) and Bazzite F43 (FFmpeg 7.1)). +//! installed version and emits a per-version cfg, so one source tree spans FFmpeg 7.x/libavcodec 61, +//! 8.x/62 and 9.x/63; validated live on Ubuntu 26.04 (FFmpeg 8), Bazzite F43 (7.1) and CachyOS +//! (FFmpeg 9). The `ffmpeg-next` MAJOR is a ceiling, not a target: 8.x refused anything past +//! libavcodec 62, which is why Arch's FFmpeg 9 needed the crate bump and not just a rebuild. +//! What a given package links is decided by the BUILDER's FFmpeg, so the soname bound that keeps an +//! install honest is generated at package time — see packaging/arch/PKGBUILD. //! //! Input is a packed RGB/BGR CPU frame; `*_nvenc` accepts `rgb0`/`bgr0`/`rgba`/`bgra` //! directly and does the RGB→YUV conversion on the GPU, so the host stays off the diff --git a/crates/pf-encode/src/enc/windows/ffmpeg_win.rs b/crates/pf-encode/src/enc/windows/ffmpeg_win.rs index 5ed6f4ac..ed428523 100644 --- a/crates/pf-encode/src/enc/windows/ffmpeg_win.rs +++ b/crates/pf-encode/src/enc/windows/ffmpeg_win.rs @@ -79,6 +79,13 @@ struct AVD3D11VADeviceContext { lock: *mut c_void, // void (*)(void*) unlock: *mut c_void, // void (*)(void*) lock_ctx: *mut c_void, + // DELIBERATELY TRUNCATED: FFmpeg >=8 appends `UINT BindFlags; UINT MiscFlags;` here, FFmpeg 7.1 + // does not, and we build against both (Windows links the BtbN n7.1 tree, Linux the distro's 8 or + // 9). Mirroring only the common prefix is what makes one definition correct for all three — + // libav owns the allocation (av_hwdevice_ctx_alloc sizes it), and we only ever WRITE `device` at + // offset 0, so a short mirror can never read or write past what libav allocated. Adding the two + // flags to match 8/9 would silently mis-describe the 7.1 build we actually ship on Windows. + // The per-pool AVD3D11VAFramesContext.BindFlags below — which we DO set — exists in all three. } /// `AVD3D11VAFramesContext` (libavutil/hwcontext_d3d11va.h) — mirrored. `BindFlags`/`MiscFlags` @@ -95,10 +102,17 @@ struct AVD3D11VAFramesContext { // Hand-written mirrors of libav's `AVD3D11VADeviceContext` / `AVD3D11VAFramesContext` // (hwcontext_d3d11va.h) — `ffmpeg-sys-next` binds neither, and we WRITE `device` / `bind_flags` // through them, so a wrong offset is silent corruption of libav's context rather than a compile -// error. ⚠ These two structs are duplicated in the other crate that talks to the same libav -// contexts (pf-encode's `ffmpeg_win.rs` and pf-client-core's `video_d3d11.rs`); they must agree -// with libav AND with each other, and these assertions are what makes a drift in either a build -// failure instead of a runtime mystery. +// error. +// +// ⚠ KNOW WHAT THESE ASSERTIONS DO AND DO NOT BUY YOU. They pin OUR layout, not libav's, so they +// turn an accidental edit to the structs above into a build failure — but nothing here reads +// hwcontext_d3d11va.h, so a field libav inserts upstream still sails straight through, and a green +// build is not evidence. Both layouts were therefore re-checked BY HAND against FFmpeg 7.1, 8.1.2 +// and 9.0 during the 8 -> 9 bump (2026-08-08): `AVD3D11VAFramesContext` is byte-identical in all +// three, and `AVD3D11VADeviceContext` gained two trailing UINTs in 8 that 7.1 lacks — which is +// exactly why the struct above stops at the common prefix. Re-check by hand on the next FFmpeg +// major. (An older note here claimed these were duplicated in pf-client-core's `video_d3d11.rs`; +// that copy went away with the client's FFmpeg in M10, so this is now the only definition.) const _: () = { use std::mem::{offset_of, size_of}; type P = *mut c_void; diff --git a/scripts/ci/provision-windows-punktfunk-extras.ps1 b/scripts/ci/provision-windows-punktfunk-extras.ps1 index 63fe3b42..e7a34b13 100644 --- a/scripts/ci/provision-windows-punktfunk-extras.ps1 +++ b/scripts/ci/provision-windows-punktfunk-extras.ps1 @@ -45,6 +45,17 @@ if (Test-Path $rustup) { # signed DLLs in users' installs. The pins below were captured 2026-07-10 from the then-current # n7.1 lgpl-shared build. When BtbN re-rolls `latest`, this fetch FAILS CLOSED (hash mismatch) — # that is intentional: re-download, re-verify the new archive, and update the two pins here. +# +# STILL n7.1 AFTER THE 2026-08-08 ffmpeg-next 8 -> 9 BUMP, on purpose. A crate major is a CEILING, +# not a target (ffmpeg-sys-next 9 spans libavcodec 56..63), so 7.1 keeps compiling; and Windows has +# no exposure to the soname break that forced the bump, because these DLLs are BUNDLED into the +# signed installer/MSIX rather than resolved from a system that can upgrade underneath them. BtbN +# publishes no FFmpeg 9 build at all right now (`latest` carries n7.1 and n8.1 only), so matching +# Arch is not even available. Moving this pin would swap the DLLs inside a code-signed installer and +# re-qualify AMF/QSV encode on real Intel/AMD hardware, which is its own change with its own on-glass +# pass — not a side effect of a Cargo bump. ⚠ One consequence to keep in mind while it stays here: +# 7.1's `AVD3D11VADeviceContext` is two UINTs shorter than 8/9's, which is why the mirror in +# crates/pf-encode/src/enc/windows/ffmpeg_win.rs deliberately stops at the common prefix. # Refresh a pin: (Get-FileHash .\ffmpeg-.zip -Algorithm SHA256).Hash function Get-BtbnFfmpeg { param([string]$Dir, [string]$ZipTag, [string]$Sha) # ZipTag: 'win64' (x64); BtbN also publishes 'winarm64'