Merge remote-tracking branch 'origin/main' into worktree-release-0250

This commit is contained in:
2026-08-08 02:42:00 +02:00
51 changed files with 604 additions and 236 deletions
+25
View File
@@ -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=<soname>-<arch>` 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.
+7
View File
@@ -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
Generated
+8 -8
View File
@@ -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"
+8 -8
View File
@@ -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
+10 -1
View File
@@ -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
+8
View File
@@ -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
+3 -1
View File
@@ -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)
@@ -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
@@ -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
+5 -5
View File
@@ -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
+5 -5
View File
@@ -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
+5 -1
View File
@@ -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
+28 -1
View File
@@ -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
+7 -3
View File
@@ -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`.
+6 -2
View File
@@ -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
+18 -4
View File
@@ -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;
@@ -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,
+31 -2
View File
@@ -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=<soname>-<arch>` 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'
+46
View File
@@ -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"
}
}
}
+21
View File
@@ -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=="],
+4 -3
View File
@@ -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);
/**
* `<kind>/<slug>.jpg` across the current, legacy-cache and Flatpak Lutris roots.
+14 -4
View File
@@ -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",
@@ -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",
+3 -9
View File
@@ -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<S extends Schema.Top> {
readonly get: Effect.Effect<S["Type"]>;
@@ -44,20 +44,14 @@ export const makeCacheStore = <S extends Schema.Top>(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 = <A>(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),
+3 -7
View File
@@ -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 <E, R>(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 <E, R>(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 {
+3 -8
View File
@@ -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<S extends Schema.Top> {
/** Decode the raw file with Schema defaults applied. Missing file → all defaults. */
@@ -36,10 +36,7 @@ export interface ConfigService<S extends Schema.Top> {
*/
readonly saveRaw: (
raw: unknown,
) => Effect.Effect<
S["Type"],
ConfigParseError | ConfigWriteError
>;
) => Effect.Effect<S["Type"], ConfigParseError | ConfigWriteError>;
/** Emits the decoded config after every successful `saveRaw`. */
readonly changes: Stream.Stream<S["Type"]>;
/** Absolute path of the config file (status views). */
@@ -95,9 +92,7 @@ export const makeConfigService = <S extends Schema.Top>(opts: {
const file = statePath(info.name, opts.fileName ?? "config.json");
const hub = yield* PubSub.unbounded<S["Type"]>();
const decode = (
raw: unknown,
): Effect.Effect<S["Type"], ConfigParseError> =>
const decode = (raw: unknown): Effect.Effect<S["Type"], ConfigParseError> =>
Schema.decodeUnknownEffect(opts.schema)(raw).pipe(
Effect.mapError(
(e) => new ConfigParseError({ path: file, issue: String(e) }),
+9 -9
View File
@@ -21,14 +21,13 @@ export interface HostClientService {
readonly facade: Punktfunk;
}
export class HostClient extends Context.Service<HostClient, HostClientService>()(
"@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<HostClient> =>
export const hostClientFromFacade = (pf: Punktfunk): Layer.Layer<HostClient> =>
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<PluginInfo, PluginInfoService>()(
"@punktfunk/plugin-kit/PluginInfo",
) {}
export class PluginInfo extends Context.Service<
PluginInfo,
PluginInfoService
>()("@punktfunk/plugin-kit/PluginInfo") {}
export const pluginInfoLayer = (
info: PluginInfoService,
+15 -14
View File
@@ -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";
+16 -10
View File
@@ -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 = <S extends Schema.Top>(
const debounce = def.debounce ?? Duration.seconds(3);
/** The config service, built fresh wherever it is needed (it only requires `PluginInfo`). */
const config: Effect.Effect<ConfigService<S>, never, PluginInfo> =
makeConfigService({ schema: def.configSchema });
const config: Effect.Effect<
ConfigService<S>,
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 = <S extends Schema.Top>(
}),
},
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 = <S extends Schema.Top>(
}
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<typeof fromHostEntry>[],
catch: (cause) => new Error(`cannot read ${compare}: ${cause}`),
});
const cfg = yield* (yield* config).load;
@@ -307,7 +312,8 @@ export const defineLibraryPlugin = <S extends Schema.Top>(
}),
},
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;
+10 -2
View File
@@ -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;
+1 -1
View File
@@ -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 !== "."));
+2 -1
View File
@@ -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. */
+3 -2
View File
@@ -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,
+36 -7
View File
@@ -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 <key>` 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);
};
/**
+5 -2
View File
@@ -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;
};
/**
+4 -2
View File
@@ -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. */
+3 -4
View File
@@ -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/<id>` when served through the console proxy, "" in dev/standalone. */
export const resolvePluginBase = (): string => {
@@ -110,9 +111,7 @@ export interface ResultGateProps<A, E> {
* 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 = <A, E>(
props: ResultGateProps<A, E>,
): ReactNode => {
export const ResultGate = <A, E>(props: ResultGateProps<A, E>): ReactNode => {
const { result } = props;
if (AsyncResult.isSuccess(result)) return props.children(result.value);
if (AsyncResult.isFailure(result)) {
+3 -1
View File
@@ -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<void, HostRequestError>;
readonly remove: (
providerId: string,
) => Effect.Effect<void, HostRequestError>;
}
export class ProviderClient extends Context.Service<
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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: <name>` 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();
+6 -5
View File
@@ -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<Report> =
| { 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<Report>;
});
+5 -5
View File
@@ -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<Response | undefined> => {
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 }),
+1 -4
View File
@@ -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);
+28 -12
View File
@@ -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 <A>(
use: (handler: (req: Request) => Promise<Response>, file: string) => Promise<A>,
use: (
handler: (req: Request) => Promise<Response>,
file: string,
) => Promise<A>,
): Promise<A> => {
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" }),
),
),
),
);
+2 -1
View File
@@ -40,7 +40,8 @@ const pluginEntry = (over: Partial<ProviderEntry> = {}): 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,
+114 -20
View File
@@ -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([]);
});
});
+2 -6
View File
@@ -45,9 +45,7 @@ describe("spike 2: client prefix through the console proxy", () => {
const captured: Array<string> = [];
class Api extends AtomHttpApi.Service<Api>()("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<string> = [];
class Api extends AtomHttpApi.Service<Api>()("SpikeApiBaseUrl", {
api,
httpClient: Layer.succeed(HttpClient.HttpClient)(
captureClient(captured),
),
httpClient: Layer.succeed(HttpClient.HttpClient)(captureClient(captured)),
baseUrl: PREFIX,
}) {}
+15 -9
View File
@@ -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.
+6 -2
View File
@@ -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;
+2 -6
View File
@@ -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');
+25 -27
View File
@@ -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<LastSync | undefined>(undefined);
const entries = opts?.entries ?? (() => ["a", "b"]);
const engine = yield* makeSyncEngine<Report, ReadonlyArray<string>, 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<Report, ReadonlyArray<string>, 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 };
});
@@ -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-<tag>.zip -Algorithm SHA256).Hash
function Get-BtbnFfmpeg {
param([string]$Dir, [string]$ZipTag, [string]$Sha) # ZipTag: 'win64' (x64); BtbN also publishes 'winarm64'