Field report: on NixOS 0.28, the scripting package and punktfunk-scripting.service are up and punktfunk-host plugins status reports running/enabled — but installing a plugin from the console (or punktfunk-host plugins add steam) fails with "the plugin runner isn't installed — install it first (Debian/Ubuntu: …; SteamOS: …)".
Root cause
runner_command() checked FHS locations exclusively: /usr/bin/punktfunk-scripting, the /usr/lib + /usr/share pair behind it, and the ~/.local mirror the SteamOS installer lays down.
Nix ships punktfunk-scripting as a derivation of its own (packaging/nix/packages.nix:446 — makeWrapper → $out/bin/punktfunk-scripting), so its wrapper is neither beside the host binary nor anywhere under /usr. Nothing the resolver looked at could ever match.
Service ops (enable/disable/status) go through systemd and were unaffected, which is what made the failure read as arbitrary: the runner demonstrably worked, and only the half that had to locate the executable was blind.
The fix — two halves, both required
1. Resolution now matches punktfunk-encode-worker's:
PUNKTFUNK_SCRIPTING → beside the host binary → PATH → the /usr layout → the ~/.local layout
PATH is the rung Nix lands on. The /usr rungs are kept after it rather than dropped, because a systemd unit's PATH need not include /usr/bin. As with the encode worker, an explicit PUNKTFUNK_SCRIPTING is deliberately not existence-checked — a named path that is wrong should fail naming itself, not fall through to some other runner.
Lifted into a pure, fully-injected function so the whole table is testable — which is also how the regression is pinned: removing the PATH rung fails the NixOS row specifically.
2. ⚠ Packager-visible — the NixOS module now puts the runner on the HOST UNIT's path.
This is the half without which the Rust change alone would not have fixed the console. Store jobs run from inside the host service (store/jobs.rs:494), whose PATH is exactly that unit's path list — environment.systemPackages only ever covered an operator's interactive shell. Without it the CLI would have been fixed and the console would not.
module-check.nix gains both the positive and the negative assertion, so CI's nix flake check --no-build holds the property.
Anyone packaging the host separately wants the same property: the runner must be on the service's PATH, or PUNKTFUNK_SCRIPTING set for it.
Also in here
The "not installed" text named only apt and SteamOS; it now names NixOS and the override.
/bin/true and /bin/false in two tests — NixOS ships only sh in /bin, so cargo test failed there for reasons unrelated to the code under test. gamelease resolves through PATH; pyrowave_remote cannot (its PinnedExe::open needs a real path, and a bare name would take the spawn-failure rung instead of the handshake rung the test exists to exercise), so it resolves one off PATH first.
CHANGELOG entries go in a new v0.28.1 — in development section: this branch is based on the commit v0.28.0 points at, so writing them into that section would credit a released version with changes it does not contain.
The ln -s "$(command -v punktfunk-scripting)" ~/.local/bin/punktfunk-scripting workaround is no longer needed and can be removed.
Verification
Run in the amd64 container (punktfunk-rust-ci), against punktfunk-host:
cargo test -p punktfunk-host plugins::tests → 9 passed, including both new tests
cargo test -p punktfunk-host gamelease::…a_launcher_that_hands_off… → passed on the new PATH-resolved spawn
cargo fmt --check → clean
The resolution table was checked non-vacuously: planting the original bug (deleting the PATH rung) fails it at exactly the reported symptom — a runner at /run/current-system/sw/bin resolving to None.
⚠ One gap left to CI: pf-encode's pyrowave_remote::tests was not compiled in-crate locally (the container needs a 1.3 GB windows-rs clone per run and the network degraded badly). The new helper was verified standalone — it compiles, resolves /usr/bin/false, and exits 1 — and the change is #[cfg(test)]-only, so a failure would surface immediately here.
Related, NOT fixed here
A sweep for the same bug class found one remaining gap: pf-dm-helper (gamescope.rs:2193) is the last executable lookup with no escape hatch — two hardcoded /usr paths, no env override, no PATH rung, and a "install the punktfunk package" message that cannot help on NixOS. It is dormant today because packaging/nix/ ships no helper at all. Left alone deliberately: its path is coupled to the polkit exec.path annotation, so whether the right ladder is PUNKTFUNK_DM_HELPER alone or also a PATH rung is a design call worth making on purpose.
Everything else checked out benign — install-kind detection already special-cases /nix/store before any marker, os-release reads both spec paths, the SteamOS/Nobara paths are unreachable on Nix, and the update-helper probes correctly read false.
Field report: on NixOS 0.28, the scripting package and `punktfunk-scripting.service` are up and `punktfunk-host plugins status` reports running/enabled — but installing a plugin from the console (or `punktfunk-host plugins add steam`) fails with *"the plugin runner isn't installed — install it first (Debian/Ubuntu: …; SteamOS: …)"*.
## Root cause
`runner_command()` checked FHS locations **exclusively**: `/usr/bin/punktfunk-scripting`, the `/usr/lib` + `/usr/share` pair behind it, and the `~/.local` mirror the SteamOS installer lays down.
Nix ships `punktfunk-scripting` as a **derivation of its own** (`packaging/nix/packages.nix:446` — `makeWrapper` → `$out/bin/punktfunk-scripting`), so its wrapper is neither beside the host binary nor anywhere under `/usr`. Nothing the resolver looked at could ever match.
Service ops (`enable`/`disable`/`status`) go through systemd and were unaffected, which is what made the failure read as arbitrary: the runner demonstrably worked, and only the half that had to *locate the executable* was blind.
## The fix — two halves, both required
**1. Resolution now matches `punktfunk-encode-worker`'s:**
> `PUNKTFUNK_SCRIPTING` → beside the host binary → `PATH` → the `/usr` layout → the `~/.local` layout
`PATH` is the rung Nix lands on. The `/usr` rungs are kept **after** it rather than dropped, because a systemd unit's `PATH` need not include `/usr/bin`. As with the encode worker, an explicit `PUNKTFUNK_SCRIPTING` is deliberately **not** existence-checked — a named path that is wrong should fail naming itself, not fall through to some other runner.
Lifted into a pure, fully-injected function so the whole table is testable — which is also how the regression is pinned: removing the `PATH` rung fails the NixOS row specifically.
**2. ⚠ Packager-visible — the NixOS module now puts the runner on the HOST UNIT's `path`.**
This is the half without which the Rust change alone would not have fixed the console. Store jobs run from *inside* the host service (`store/jobs.rs:494`), whose `PATH` is exactly that unit's `path` list — `environment.systemPackages` only ever covered an operator's interactive shell. Without it the CLI would have been fixed and the console would not.
`module-check.nix` gains both the positive and the negative assertion, so CI's `nix flake check --no-build` holds the property.
Anyone packaging the host separately wants the same property: the runner must be on the service's `PATH`, or `PUNKTFUNK_SCRIPTING` set for it.
## Also in here
- The "not installed" text named only `apt` and SteamOS; it now names NixOS and the override.
- `/bin/true` and `/bin/false` in two tests — NixOS ships only `sh` in `/bin`, so `cargo test` failed there for reasons unrelated to the code under test. `gamelease` resolves through `PATH`; `pyrowave_remote` cannot (its `PinnedExe::open` needs a real path, and a bare name would take the spawn-failure rung instead of the handshake rung the test exists to exercise), so it resolves one off `PATH` first.
- CHANGELOG entries go in a new `v0.28.1 — in development` section: this branch is based on the commit `v0.28.0` points at, so writing them into that section would credit a released version with changes it does not contain.
The `ln -s "$(command -v punktfunk-scripting)" ~/.local/bin/punktfunk-scripting` workaround is no longer needed and can be removed.
## Verification
Run in the amd64 container (`punktfunk-rust-ci`), against `punktfunk-host`:
- `cargo clippy --all-targets -p punktfunk-host -- -D warnings` → **exit 0**
- `cargo test -p punktfunk-host plugins::tests` → **9 passed**, including both new tests
- `cargo test -p punktfunk-host gamelease::…a_launcher_that_hands_off…` → **passed** on the new PATH-resolved spawn
- `cargo fmt --check` → clean
The resolution table was checked **non-vacuously**: planting the original bug (deleting the `PATH` rung) fails it at exactly the reported symptom — a runner at `/run/current-system/sw/bin` resolving to `None`.
⚠ One gap left to CI: `pf-encode`'s `pyrowave_remote::tests` was not compiled in-crate locally (the container needs a 1.3 GB `windows-rs` clone per run and the network degraded badly). The new helper was verified standalone — it compiles, resolves `/usr/bin/false`, and exits 1 — and the change is `#[cfg(test)]`-only, so a failure would surface immediately here.
## Related, NOT fixed here
A sweep for the same bug class found one remaining gap: **`pf-dm-helper`** (`gamescope.rs:2193`) is the last executable lookup with no escape hatch — two hardcoded `/usr` paths, no env override, no `PATH` rung, and a "install the punktfunk package" message that cannot help on NixOS. It is dormant today because `packaging/nix/` ships no helper at all. Left alone deliberately: its path is coupled to the polkit `exec.path` annotation, so whether the right ladder is `PUNKTFUNK_DM_HELPER` alone or also a `PATH` rung is a design call worth making on purpose.
Everything else checked out benign — install-kind detection already special-cases `/nix/store` before any marker, `os-release` reads both spec paths, the SteamOS/Nobara paths are unreachable on Nix, and the update-helper probes correctly read `false`.
On NixOS every plugin PACKAGE op failed with "the plugin runner isn't
installed" on a box where the runner was installed, enabled and running.
`runner_command()` checked FHS locations exclusively — /usr/bin, the
/usr/lib + /usr/share pair behind it, and the ~/.local mirror the SteamOS
installer lays down. Nix ships punktfunk-scripting as a derivation of its
OWN, so its wrapper is neither beside the host binary nor anywhere under
/usr, and no rung could ever match. Service ops go through systemd and
were unaffected, which is what made it read as arbitrary: `plugins
status` said running/enabled while `plugins add` said not installed.
Resolution now matches punktfunk-encode-worker's: PUNKTFUNK_SCRIPTING ->
beside the host binary -> PATH -> /usr -> ~/.local. PATH is the rung Nix
lands on. The /usr rungs stay AFTER it rather than being dropped, because
a systemd unit's PATH need not include /usr/bin. As with the encode
worker the env override is deliberately not existence-checked — a named
path that is wrong should fail naming itself, not fall through to some
other runner. Lifted into a pure injected function so the whole table is
testable, which is also how the regression is pinned: removing the PATH
rung fails the NixOS row specifically.
Second half, and the reason the Rust change alone would not have fixed
the console: the NixOS module now puts the runner on the HOST UNIT's
`path`. The console installs plugins from inside the host service, whose
PATH is exactly that unit list — `environment.systemPackages` only ever
covered an operator's interactive shell. Without it the CLI would have
been fixed and the console would not. module-check.nix gains both the
positive and the negative assertion, so CI's `nix flake check --no-build`
holds the property.
The error text named only apt and SteamOS; it now names NixOS and the
override. The ~/.local/bin symlink workaround is no longer needed.
Two tests hardcoded absolute paths into /bin, which on NixOS holds only
`sh` — so `cargo test` failed there for reasons that had nothing to do
with the code under test.
`gamelease` only needs a process that exits quickly and successfully, so
the bare name resolved through PATH is exactly right.
`pyrowave_remote` cannot use a bare name: `spawn_link` pins the binary
with `PinnedExe::open`, so a name that PATH would have resolved fails the
OPEN instead — which takes the spawn-failure rung rather than the
handshake rung the test exists to exercise. It resolves a real path off
PATH first, keeping the test on the rung it names.
The branch is based on the commit v0.28.0 points at, so writing the NixOS
runner fix into that section would have credited a released version with
a change it does not contain. Moved to a fresh `v0.28.1 — in development`
section, matching how the v0.28.0 cycle was kept (a `— in development`
heading the release commit renames).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Field report: on NixOS 0.28, the scripting package and
punktfunk-scripting.serviceare up andpunktfunk-host plugins statusreports running/enabled — but installing a plugin from the console (orpunktfunk-host plugins add steam) fails with "the plugin runner isn't installed — install it first (Debian/Ubuntu: …; SteamOS: …)".Root cause
runner_command()checked FHS locations exclusively:/usr/bin/punktfunk-scripting, the/usr/lib+/usr/sharepair behind it, and the~/.localmirror the SteamOS installer lays down.Nix ships
punktfunk-scriptingas a derivation of its own (packaging/nix/packages.nix:446—makeWrapper→$out/bin/punktfunk-scripting), so its wrapper is neither beside the host binary nor anywhere under/usr. Nothing the resolver looked at could ever match.Service ops (
enable/disable/status) go through systemd and were unaffected, which is what made the failure read as arbitrary: the runner demonstrably worked, and only the half that had to locate the executable was blind.The fix — two halves, both required
1. Resolution now matches
punktfunk-encode-worker's:PATHis the rung Nix lands on. The/usrrungs are kept after it rather than dropped, because a systemd unit'sPATHneed not include/usr/bin. As with the encode worker, an explicitPUNKTFUNK_SCRIPTINGis deliberately not existence-checked — a named path that is wrong should fail naming itself, not fall through to some other runner.Lifted into a pure, fully-injected function so the whole table is testable — which is also how the regression is pinned: removing the
PATHrung fails the NixOS row specifically.2. ⚠ Packager-visible — the NixOS module now puts the runner on the HOST UNIT's
path.This is the half without which the Rust change alone would not have fixed the console. Store jobs run from inside the host service (
store/jobs.rs:494), whosePATHis exactly that unit'spathlist —environment.systemPackagesonly ever covered an operator's interactive shell. Without it the CLI would have been fixed and the console would not.module-check.nixgains both the positive and the negative assertion, so CI'snix flake check --no-buildholds the property.Anyone packaging the host separately wants the same property: the runner must be on the service's
PATH, orPUNKTFUNK_SCRIPTINGset for it.Also in here
aptand SteamOS; it now names NixOS and the override./bin/trueand/bin/falsein two tests — NixOS ships onlyshin/bin, socargo testfailed there for reasons unrelated to the code under test.gameleaseresolves throughPATH;pyrowave_remotecannot (itsPinnedExe::openneeds a real path, and a bare name would take the spawn-failure rung instead of the handshake rung the test exists to exercise), so it resolves one offPATHfirst.v0.28.1 — in developmentsection: this branch is based on the commitv0.28.0points at, so writing them into that section would credit a released version with changes it does not contain.The
ln -s "$(command -v punktfunk-scripting)" ~/.local/bin/punktfunk-scriptingworkaround is no longer needed and can be removed.Verification
Run in the amd64 container (
punktfunk-rust-ci), againstpunktfunk-host:cargo clippy --all-targets -p punktfunk-host -- -D warnings→ exit 0cargo test -p punktfunk-host plugins::tests→ 9 passed, including both new testscargo test -p punktfunk-host gamelease::…a_launcher_that_hands_off…→ passed on the new PATH-resolved spawncargo fmt --check→ cleanThe resolution table was checked non-vacuously: planting the original bug (deleting the
PATHrung) fails it at exactly the reported symptom — a runner at/run/current-system/sw/binresolving toNone.⚠ One gap left to CI:
pf-encode'spyrowave_remote::testswas not compiled in-crate locally (the container needs a 1.3 GBwindows-rsclone per run and the network degraded badly). The new helper was verified standalone — it compiles, resolves/usr/bin/false, and exits 1 — and the change is#[cfg(test)]-only, so a failure would surface immediately here.Related, NOT fixed here
A sweep for the same bug class found one remaining gap:
pf-dm-helper(gamescope.rs:2193) is the last executable lookup with no escape hatch — two hardcoded/usrpaths, no env override, noPATHrung, and a "install the punktfunk package" message that cannot help on NixOS. It is dormant today becausepackaging/nix/ships no helper at all. Left alone deliberately: its path is coupled to the polkitexec.pathannotation, so whether the right ladder isPUNKTFUNK_DM_HELPERalone or also aPATHrung is a design call worth making on purpose.Everything else checked out benign — install-kind detection already special-cases
/nix/storebefore any marker,os-releasereads both spec paths, the SteamOS/Nobara paths are unreachable on Nix, and the update-helper probes correctly readfalse.