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