The plugin runner was located by FHS path only, so NixOS never found it #207

Merged
enricobuehler merged 3 commits from worktree-nix-plugin-runner-resolve into main 2026-08-13 20:10:16 +00:00
Owner

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:446makeWrapper$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 warningsexit 0
  • cargo test -p punktfunk-host plugins::tests9 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`.
enricobuehler added 3 commits 2026-08-13 19:36:41 +00:00
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.
docs(changelog): v0.28.0 was tagged, so these fixes belong in a v0.28.1 section
ci / rust (pull_request) Successful in 4m44s
ci / bun-nix (pull_request) Successful in 28s
ci / rust-arm64 (pull_request) Successful in 4m32s
ci / docs-site (pull_request) Successful in 1m30s
ci / web (pull_request) Successful in 5m54s
nix / flake (pull_request) Successful in 15m13s
android / android (pull_request) Failing after 1m10s
c12476736d
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).
enricobuehler merged commit 05a08b9804 into main 2026-08-13 20:10:16 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#207