Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c12476736d | ||
|
|
5bcee83c34 | ||
|
|
4ebe7d1185 | ||
|
|
7df321f459 |
@@ -12,6 +12,48 @@ with the version table of the release you are moving to, then read **Breaking ch
|
||||
|
||||
---
|
||||
|
||||
## v0.28.1 — in development
|
||||
|
||||
### NixOS — the plugin runner was installed, running, and reported missing
|
||||
|
||||
🛑 **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.** `punktfunk-host plugins status` said so, and
|
||||
the console's Plugins screen still refused to install anything.
|
||||
|
||||
The host resolved `punktfunk-scripting` by checking 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 installs a wrapper at `$out/bin/punktfunk-scripting` in
|
||||
a **derivation of its own**, so it is neither beside the host binary nor anywhere under `/usr`, and
|
||||
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.
|
||||
|
||||
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. The "not installed" text now also names NixOS and the override, instead of pointing
|
||||
every operator at `apt`.
|
||||
|
||||
⚠ **Packager-visible, and the other half of the fix:** the NixOS module now puts
|
||||
`services.punktfunk.scripting.package` on the **host unit's** `path`. `environment.systemPackages`
|
||||
only ever covered an operator's interactive shell, and the console installs plugins from *inside*
|
||||
the host service — whose `PATH` is exactly that unit list. Without it the CLI would have been fixed
|
||||
and the console would not. Anyone packaging the host separately wants the same property: the runner
|
||||
must be on the service's `PATH`, or `PUNKTFUNK_SCRIPTING` set for it.
|
||||
|
||||
The `ln -s "$(command -v punktfunk-scripting)" ~/.local/bin/punktfunk-scripting` workaround is no
|
||||
longer needed and can be removed.
|
||||
|
||||
### `/bin/true` and `/bin/false` are not portable — two tests failed on NixOS
|
||||
|
||||
NixOS ships only `sh` in `/bin`, so `gamelease`'s hand-off test and `pyrowave_remote`'s
|
||||
handshake-rung test failed there for reasons unrelated to the code under test. Both now resolve a
|
||||
real binary rather than assuming an FHS path.
|
||||
|
||||
---
|
||||
|
||||
## v0.28.0
|
||||
|
||||
180 commits since v0.27.0.
|
||||
|
||||
@@ -1016,11 +1016,27 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Ladder rung: the binary exists and runs but is not a worker. `/bin/false` exits at once, so
|
||||
/// the handshake reads EOF — the same rung a worker that dies during Vulkan bring-up takes.
|
||||
/// A binary that exists, execs, and exits at once. Resolved off `PATH` rather than hardcoded
|
||||
/// to `/bin/false`: NixOS ships only `/bin/sh` in `/bin`, and `PinnedExe::open` needs a real
|
||||
/// path (so a bare name cannot stand in for one — it would fail the OPEN and take the
|
||||
/// spawn-failure rung instead of the handshake rung this exercises).
|
||||
fn a_binary_that_exits_immediately() -> PathBuf {
|
||||
std::env::var_os("PATH")
|
||||
.as_deref()
|
||||
.map(std::env::split_paths)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|d| d.join("false"))
|
||||
.find(|p| p.is_file())
|
||||
.expect("a `false` binary on PATH")
|
||||
}
|
||||
|
||||
/// Ladder rung: the binary exists and runs but is not a worker. It exits at once, so the
|
||||
/// handshake reads EOF — the same rung a worker that dies during Vulkan bring-up takes.
|
||||
#[test]
|
||||
fn a_worker_that_exits_immediately_is_a_handshake_failure() {
|
||||
let err = spawn_link(Path::new("/bin/false"), ¶ms(), 40_000_000).unwrap_err();
|
||||
let err =
|
||||
spawn_link(&a_binary_that_exits_immediately(), ¶ms(), 40_000_000).unwrap_err();
|
||||
let text = format!("{err:#}");
|
||||
assert!(
|
||||
text.contains("handshake"),
|
||||
|
||||
@@ -1410,7 +1410,9 @@ mod tests {
|
||||
|
||||
static EXITS: AtomicUsize = AtomicUsize::new(0);
|
||||
EXITS.store(0, Ordering::SeqCst);
|
||||
let child = std::process::Command::new("/bin/true")
|
||||
// Resolved through PATH, not `/bin/true`: NixOS ships only `/bin/sh` in `/bin`, so the
|
||||
// absolute path made this test — and nothing else about the code under test — fail there.
|
||||
let child = std::process::Command::new("true")
|
||||
.spawn()
|
||||
.expect("spawn the fake launcher");
|
||||
let lease = open(
|
||||
|
||||
@@ -35,6 +35,12 @@ const UNIT: &str = "punktfunk-scripting";
|
||||
#[cfg(target_os = "windows")]
|
||||
const TASK: &str = "PunktfunkScripting";
|
||||
|
||||
/// The runner executable's name. Every non-Windows package installs a wrapper under exactly this
|
||||
/// name — the deb/rpm at `/usr/bin`, the SteamOS installer at `~/.local/bin`, Nix at
|
||||
/// `$out/bin` — so one name covers every layout the resolver walks.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const RUNNER_BIN: &str = "punktfunk-scripting";
|
||||
|
||||
pub fn main(args: &[String]) -> Result<()> {
|
||||
match args.first().map(String::as_str) {
|
||||
Some("add") | Some("remove") | Some("rm") | Some("uninstall") | Some("list")
|
||||
@@ -161,41 +167,103 @@ pub(crate) fn runner_command() -> Result<(std::path::PathBuf, Vec<String>)> {
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
// The scripting package ships /usr/bin/punktfunk-scripting, a wrapper that runs the bundled
|
||||
// bun on the runner bundle and forwards "$@" (packaging/debian/build-scripting-deb.sh).
|
||||
let wrapper = std::path::PathBuf::from("/usr/bin/punktfunk-scripting");
|
||||
if wrapper.exists() {
|
||||
return Ok((wrapper, Vec::new()));
|
||||
}
|
||||
// Fall back to the package's private layout in case the wrapper is absent.
|
||||
let bun = std::path::PathBuf::from("/usr/lib/punktfunk-scripting/bun");
|
||||
let runner = std::path::PathBuf::from("/usr/share/punktfunk-scripting/runner-cli.js");
|
||||
if bun.exists() && runner.exists() {
|
||||
return Ok((bun, vec![runner.to_string_lossy().into_owned()]));
|
||||
}
|
||||
// Immutable-/usr distros (SteamOS): scripts/steamdeck/install.sh lays the SAME payload
|
||||
// out user-scoped under ~/.local — wrapper, private bun, and bundle mirroring the deb's
|
||||
// /usr layout — because a system package can't exist there.
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
let home = std::path::Path::new(&home);
|
||||
let wrapper = home.join(".local/bin/punktfunk-scripting");
|
||||
if wrapper.exists() {
|
||||
return Ok((wrapper, Vec::new()));
|
||||
}
|
||||
let bun = home.join(".local/lib/punktfunk-scripting/bun");
|
||||
let runner = home.join(".local/share/punktfunk-scripting/runner-cli.js");
|
||||
if bun.exists() && runner.exists() {
|
||||
return Ok((bun, vec![runner.to_string_lossy().into_owned()]));
|
||||
}
|
||||
}
|
||||
bail!(
|
||||
"the plugin runner isn't installed — install it first (Debian/Ubuntu: \
|
||||
`sudo apt install punktfunk-scripting`; SteamOS: re-run \
|
||||
scripts/steamdeck/install.sh)"
|
||||
let exe = std::env::current_exe().ok();
|
||||
let path_var = std::env::var("PATH").ok();
|
||||
let home = std::env::var("HOME").ok();
|
||||
resolve_runner_in(
|
||||
std::env::var("PUNKTFUNK_SCRIPTING").ok().as_deref(),
|
||||
exe.as_deref().and_then(std::path::Path::parent),
|
||||
path_var.as_deref(),
|
||||
home.as_deref().map(std::path::Path::new),
|
||||
&|p| p.is_file(),
|
||||
)
|
||||
.ok_or_else(|| anyhow::anyhow!("{RUNNER_MISSING}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// What to say when no rung matched. Shared with [`runtime_status`], so the CLI and the console
|
||||
/// tell an operator the same thing.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) const RUNNER_MISSING: &str =
|
||||
"the plugin runner isn't installed — install it first (Debian/Ubuntu: `sudo apt install \
|
||||
punktfunk-scripting`; SteamOS: re-run scripts/steamdeck/install.sh; NixOS: enable \
|
||||
`services.punktfunk.scripting`). If it is installed somewhere else, point PUNKTFUNK_SCRIPTING \
|
||||
at the punktfunk-scripting executable.";
|
||||
|
||||
/// The rungs, in order: `PUNKTFUNK_SCRIPTING` → beside the host binary → `PATH` → the packaged
|
||||
/// `/usr` layout → the user-scoped SteamOS layout. Pure and fully injected so the table can be
|
||||
/// tested without mutating process env, which races `getenv` in parallel tests.
|
||||
///
|
||||
/// `PATH` is load-bearing rather than a nicety: it is the ONLY rung a Nix install can land on.
|
||||
/// `punktfunk-scripting` is a derivation of its own there (packaging/nix/packages.nix), so its
|
||||
/// wrapper is neither beside the host binary nor anywhere under `/usr` — the layouts this
|
||||
/// resolver used to check exclusively, which is why a fully working NixOS box reported the runner
|
||||
/// as not installed.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn resolve_runner_in(
|
||||
env: Option<&str>,
|
||||
exe_dir: Option<&std::path::Path>,
|
||||
path_var: Option<&str>,
|
||||
home: Option<&std::path::Path>,
|
||||
exists: &dyn Fn(&std::path::Path) -> bool,
|
||||
) -> Option<(std::path::PathBuf, Vec<String>)> {
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
// The two-file layout: a private bun plus the runner bundle, which the deb/rpm and the SteamOS
|
||||
// installer both lay down beside their wrapper. Only a rung when BOTH halves are present.
|
||||
let pair = |bun: PathBuf, runner: PathBuf| -> Option<(PathBuf, Vec<String>)> {
|
||||
(exists(&bun) && exists(&runner))
|
||||
.then(|| (bun, vec![runner.to_string_lossy().into_owned()]))
|
||||
};
|
||||
|
||||
// The operator's own override, and deliberately NOT existence-checked: whoever names a path is
|
||||
// entitled to a failure that names it back, where falling through to a runner that happens to
|
||||
// be installed would hide the typo behind a working install.
|
||||
if let Some(v) = env.map(str::trim).filter(|v| !v.is_empty()) {
|
||||
return Some((PathBuf::from(v), Vec::new()));
|
||||
}
|
||||
// Beside the host binary — a source tree or any relocatable layout shipping both in one prefix.
|
||||
if let Some(p) = exe_dir.map(|d| d.join(RUNNER_BIN)).filter(|p| exists(p)) {
|
||||
return Some((p, Vec::new()));
|
||||
}
|
||||
if let Some(p) = path_var
|
||||
.into_iter()
|
||||
.flat_map(|v| v.split(':'))
|
||||
.filter(|d| !d.is_empty())
|
||||
.map(|d| Path::new(d).join(RUNNER_BIN))
|
||||
.find(|p| exists(p))
|
||||
{
|
||||
return Some((p, Vec::new()));
|
||||
}
|
||||
// The packaged /usr layout (packaging/debian/build-scripting-deb.sh). Still checked explicitly
|
||||
// after `PATH` because a systemd unit can carry a PATH that does not include /usr/bin.
|
||||
let wrapper = Path::new("/usr/bin").join(RUNNER_BIN);
|
||||
if exists(&wrapper) {
|
||||
return Some((wrapper, Vec::new()));
|
||||
}
|
||||
if let Some(cmd) = pair(
|
||||
Path::new("/usr/lib").join(RUNNER_BIN).join("bun"),
|
||||
Path::new("/usr/share")
|
||||
.join(RUNNER_BIN)
|
||||
.join("runner-cli.js"),
|
||||
) {
|
||||
return Some(cmd);
|
||||
}
|
||||
// Immutable-/usr distros (SteamOS): scripts/steamdeck/install.sh lays the SAME payload out
|
||||
// user-scoped under ~/.local, because a system package can't exist there.
|
||||
let home = home?;
|
||||
let wrapper = home.join(".local/bin").join(RUNNER_BIN);
|
||||
if exists(&wrapper) {
|
||||
return Some((wrapper, Vec::new()));
|
||||
}
|
||||
pair(
|
||||
home.join(".local/lib").join(RUNNER_BIN).join("bun"),
|
||||
home.join(".local/share")
|
||||
.join(RUNNER_BIN)
|
||||
.join("runner-cli.js"),
|
||||
)
|
||||
}
|
||||
|
||||
// ---- service ops ------------------------------------------------------------------------------
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -277,9 +345,7 @@ pub(crate) fn runtime_status() -> RuntimeStatus {
|
||||
detail: if installed {
|
||||
String::new()
|
||||
} else {
|
||||
"the plugin runner package isn't installed (Debian/Ubuntu: `sudo apt install \
|
||||
punktfunk-scripting`)"
|
||||
.into()
|
||||
RUNNER_MISSING.into()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -807,3 +873,151 @@ fn enable() -> Result<()> {
|
||||
fn disable() -> Result<()> {
|
||||
bail!("the plugin runner is only available on Linux and Windows hosts")
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_os = "windows")))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Build an `exists` probe over a fixed set of paths.
|
||||
fn present(ps: Vec<PathBuf>) -> impl Fn(&Path) -> bool {
|
||||
move |p: &Path| ps.iter().any(|q| q == p)
|
||||
}
|
||||
|
||||
/// Every layout the resolver has to serve, in one table — the regression guard for the NixOS
|
||||
/// report (a runner on `PATH` and nowhere else read as "not installed").
|
||||
#[test]
|
||||
fn runner_resolution_table() {
|
||||
let beside = Path::new("/opt/punktfunk/bin");
|
||||
let nix = Path::new("/run/current-system/sw/bin");
|
||||
let home = Path::new("/home/deck");
|
||||
|
||||
// The rung a Nix install lands on: NOT beside the host binary, NOT under /usr — `PATH`
|
||||
// only. This is the whole bug.
|
||||
let exists = present(vec![nix.join(RUNNER_BIN)]);
|
||||
assert_eq!(
|
||||
resolve_runner_in(
|
||||
None,
|
||||
Some(beside),
|
||||
Some(nix.to_str().unwrap()),
|
||||
None,
|
||||
&exists
|
||||
),
|
||||
Some((nix.join(RUNNER_BIN), Vec::new()))
|
||||
);
|
||||
|
||||
// An explicit override wins over every discovery…
|
||||
let exists = present(vec![beside.join(RUNNER_BIN)]);
|
||||
assert_eq!(
|
||||
resolve_runner_in(
|
||||
Some("/nix/store/abc/bin/punktfunk-scripting"),
|
||||
Some(beside),
|
||||
Some("/usr/bin"),
|
||||
Some(home),
|
||||
&exists
|
||||
),
|
||||
Some(("/nix/store/abc/bin/punktfunk-scripting".into(), Vec::new()))
|
||||
);
|
||||
// …and is not existence-checked, so a typo surfaces as a spawn failure naming the path
|
||||
// rather than silently running some other runner.
|
||||
assert_eq!(
|
||||
resolve_runner_in(Some("/nope/pf"), Some(beside), None, None, &exists),
|
||||
Some(("/nope/pf".into(), Vec::new()))
|
||||
);
|
||||
// Empty/whitespace reads as unset, not as a path.
|
||||
assert_eq!(
|
||||
resolve_runner_in(Some(" "), Some(beside), None, None, &exists),
|
||||
Some((beside.join(RUNNER_BIN), Vec::new()))
|
||||
);
|
||||
// Beside the host binary beats PATH.
|
||||
let exists = present(vec![beside.join(RUNNER_BIN), nix.join(RUNNER_BIN)]);
|
||||
assert_eq!(
|
||||
resolve_runner_in(
|
||||
None,
|
||||
Some(beside),
|
||||
Some(nix.to_str().unwrap()),
|
||||
None,
|
||||
&exists
|
||||
),
|
||||
Some((beside.join(RUNNER_BIN), Vec::new()))
|
||||
);
|
||||
// PATH is walked entry by entry, skipping empties.
|
||||
let exists = present(vec![nix.join(RUNNER_BIN)]);
|
||||
assert_eq!(
|
||||
resolve_runner_in(
|
||||
None,
|
||||
Some(Path::new("/nowhere")),
|
||||
Some(":/nope:/run/current-system/sw/bin"),
|
||||
None,
|
||||
&exists
|
||||
),
|
||||
Some((nix.join(RUNNER_BIN), Vec::new()))
|
||||
);
|
||||
|
||||
// The deb/rpm wrapper, found even when the unit's PATH omits /usr/bin.
|
||||
let exists = present(vec![PathBuf::from("/usr/bin").join(RUNNER_BIN)]);
|
||||
assert_eq!(
|
||||
resolve_runner_in(
|
||||
None,
|
||||
Some(Path::new("/nowhere")),
|
||||
Some("/nope"),
|
||||
None,
|
||||
&exists
|
||||
),
|
||||
Some((PathBuf::from("/usr/bin").join(RUNNER_BIN), Vec::new()))
|
||||
);
|
||||
// …and its private two-file layout when the wrapper is absent.
|
||||
let bun = PathBuf::from("/usr/lib").join(RUNNER_BIN).join("bun");
|
||||
let cli = PathBuf::from("/usr/share")
|
||||
.join(RUNNER_BIN)
|
||||
.join("runner-cli.js");
|
||||
let exists = present(vec![bun.clone(), cli.clone()]);
|
||||
assert_eq!(
|
||||
resolve_runner_in(None, None, None, None, &exists),
|
||||
Some((bun, vec![cli.to_string_lossy().into_owned()]))
|
||||
);
|
||||
// Half of that layout is not a rung — a partial install must fall through, not spawn a
|
||||
// bun with no script.
|
||||
let exists = present(vec![PathBuf::from("/usr/lib").join(RUNNER_BIN).join("bun")]);
|
||||
assert_eq!(resolve_runner_in(None, None, None, None, &exists), None);
|
||||
|
||||
// SteamOS: the same payload, user-scoped. Reached only via HOME.
|
||||
let exists = present(vec![home.join(".local/bin").join(RUNNER_BIN)]);
|
||||
assert_eq!(
|
||||
resolve_runner_in(None, None, None, Some(home), &exists),
|
||||
Some((home.join(".local/bin").join(RUNNER_BIN), Vec::new()))
|
||||
);
|
||||
assert_eq!(resolve_runner_in(None, None, None, None, &exists), None);
|
||||
let bun = home.join(".local/lib").join(RUNNER_BIN).join("bun");
|
||||
let cli = home
|
||||
.join(".local/share")
|
||||
.join(RUNNER_BIN)
|
||||
.join("runner-cli.js");
|
||||
let exists = present(vec![bun.clone(), cli.clone()]);
|
||||
assert_eq!(
|
||||
resolve_runner_in(None, None, None, Some(home), &exists),
|
||||
Some((bun, vec![cli.to_string_lossy().into_owned()]))
|
||||
);
|
||||
|
||||
// Nothing anywhere: the "not installed" rung the error text speaks for.
|
||||
let exists = present(vec![]);
|
||||
assert_eq!(
|
||||
resolve_runner_in(None, Some(beside), Some("/nope"), Some(home), &exists),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// The operator-facing miss must name NixOS — the report's second half was that the error
|
||||
/// pointed a NixOS operator at `apt`.
|
||||
#[test]
|
||||
fn the_missing_runner_error_names_every_platform_it_can_be_installed_on() {
|
||||
for hint in [
|
||||
"apt install",
|
||||
"steamdeck/install.sh",
|
||||
"NixOS",
|
||||
"PUNKTFUNK_SCRIPTING",
|
||||
] {
|
||||
assert!(RUNNER_MISSING.contains(hint), "missing hint: {hint}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +244,7 @@ notes for context.
|
||||
| `PUNKTFUNK_IDD_DEPTH` | `N` (default `2`) | **(Windows)** IDD-push pipeline depth. `1` cuts latency once GPU priority is raised; higher smooths a contended GPU. |
|
||||
| `PYROWAVE_QUEUE_PRIORITY` | `realtime` *(default)* · `high` · `off` | [PyroWave](/docs/pyrowave) sessions only — the *intent*, forwarded to whichever process does the encode. PyroWave encodes on the same GPU shader cores a game uses, so a demanding game can starve it and the frame rate drops. This asks the driver to schedule the encode ahead of the game. `realtime` tries the strongest class and falls back to `high`; `high` asks only for the middle one; `off` disables the request. A driver that refuses simply encodes at normal priority — it can never stop a session starting. Granting the request needs the `CAP_SYS_NICE` capability, which the Linux packages give to `punktfunk-encode-worker` and **never** to `punktfunk-host` — a host holding any capability cannot be identified by KWin and loses desktop streaming entirely. Do not `setcap` the host to "make this work"; see [Running as a service](/docs/running-as-a-service#gpu-scheduling-priority). Set `off` if you see the desktop stutter while streaming. |
|
||||
| `PUNKTFUNK_ENCODE_WORKER` | path · `off` | Where the host looks for `punktfunk-encode-worker`, the small capability-carrying helper that owns the priority-elevated [PyroWave](/docs/pyrowave) encode (previous row). Unset, the host looks beside its own binary and then on `PATH`, which is right for every package — set it only when the worker lives somewhere unusual. **NixOS needs it and the module sets it for you:** a file capability cannot live on a read-only nix store path, so the worker is exposed through `security.wrappers` and this points the host at that wrapper. `off` forces the encode back into the host process at default priority — a debug escape hatch, not a tuning knob. Every failure short of that is already handled: a missing binary, a worker that will not start, or one that dies mid-session falls back to encoding in-process with one line in the log, and never drops the session. |
|
||||
| `PUNKTFUNK_SCRIPTING` | path | Where the host looks for `punktfunk-scripting`, the runner that performs every [plugin](/docs/plugins) package op (`plugins add`/`remove`/`list`, and the console's store installs). Unset, the host looks beside its own binary, then on `PATH`, then in the packaged `/usr` and `~/.local` layouts — right for every package, so set it only when the runner lives somewhere unusual. Like the row above it is **not** existence-checked: a path you name is a path you get, so a typo fails naming itself instead of quietly running a different runner. Worth knowing: the console runs installs inside the host *service*, whose `PATH` is normally much shorter than your login shell's — if `punktfunk-host plugins add` works and the console says the runner isn't installed, that gap is why, and this is the fix. |
|
||||
|
||||
## Diagnostics
|
||||
|
||||
|
||||
@@ -305,7 +305,16 @@ installer's `PATH` change, or call the exe by full path. On Linux the host packa
|
||||
[same RPM repo you installed the host from](/docs/fedora). On Arch:
|
||||
`sudo pacman -Syu punktfunk-scripting` (a full `-Syu`, like every other install from that repo).
|
||||
On SteamOS, re-run `scripts/steamdeck/install.sh` (or
|
||||
`scripts/steamdeck/update.sh`). On Windows, re-run the installer and keep the scripting component.
|
||||
`scripts/steamdeck/update.sh`). On NixOS it comes with `services.punktfunk.scripting.enable` (on by
|
||||
default whenever the host is). On Windows, re-run the installer and keep the scripting component.
|
||||
|
||||
If the runner *is* installed and the host still says it isn't, the host could not find the
|
||||
`punktfunk-scripting` executable. It looks beside its own binary, then on `PATH`, then in the
|
||||
packaged `/usr` and `~/.local` layouts — so a runner installed somewhere else needs
|
||||
`PUNKTFUNK_SCRIPTING` pointed at it (see [Configuration](/docs/configuration)). Note that the
|
||||
console installs plugins from inside the host *service*, whose `PATH` is usually much shorter than
|
||||
your shell's: a runner that `punktfunk-host plugins add` finds and the console does not is that
|
||||
difference, and the env var is the fix.
|
||||
|
||||
**Where a plugin's log output goes** — the console's **Logs** page, under the **Plugins** filter.
|
||||
The runner ships everything your plugins print to the host, so a plugin's own lines sit next to the
|
||||
|
||||
@@ -76,8 +76,10 @@ let
|
||||
];
|
||||
}).config;
|
||||
|
||||
# Every scenario keeps `gamescopeHdr = false`: it is the one option whose default would pull a
|
||||
# real (stub, here) gamescope onto the unit PATH, and nothing below is about that.
|
||||
# Every scenario keeps `gamescopeHdr = false`: its default would pull a real (stub, here)
|
||||
# gamescope onto the host unit's PATH, and nothing below is about that. `scripting` is left at
|
||||
# its default (on with the host) precisely BECAUSE the runner belongs on that PATH — see the
|
||||
# "not just in systemPackages" check.
|
||||
desktop = evalWith {
|
||||
services.punktfunk.host = {
|
||||
enable = true;
|
||||
@@ -108,6 +110,15 @@ let
|
||||
};
|
||||
};
|
||||
|
||||
# A host that has opted out of the runner — the negative half of the PATH check.
|
||||
noScripting = evalWith {
|
||||
services.punktfunk.host = {
|
||||
enable = true;
|
||||
gamescopeHdr = false;
|
||||
};
|
||||
services.punktfunk.scripting.enable = false;
|
||||
};
|
||||
|
||||
clientOnly = evalWith { services.punktfunk.client.enable = true; };
|
||||
|
||||
unit = cfg: name: cfg.systemd.user.units."${name}.service".text;
|
||||
@@ -243,6 +254,24 @@ let
|
||||
name = "the plugin runner is started by default, like every other channel";
|
||||
ok = has appliance "punktfunk-scripting" "WantedBy=default.target";
|
||||
}
|
||||
{
|
||||
# The runner has to be on the HOST unit's PATH, not merely in systemPackages. Package ops
|
||||
# (`plugins add`, and the console's store jobs, which run inside the host process) locate
|
||||
# `punktfunk-scripting` as an executable, and on NixOS PATH is the only rung that can ever
|
||||
# match — the runner is its own derivation, so it is never beside the host binary and never
|
||||
# under /usr. systemPackages covers an operator's shell and NOT this unit, which is exactly
|
||||
# how a running, enabled runner reported itself "not installed" through the console.
|
||||
name = "the plugin runner is on the host unit's PATH, not just in systemPackages";
|
||||
ok =
|
||||
has appliance "punktfunk-host" "/pf-stub/punktfunk-scripting/bin"
|
||||
&& has desktop "punktfunk-host" "/pf-stub/punktfunk-scripting/bin";
|
||||
}
|
||||
{
|
||||
# …and only when it is actually installed, so `scripting.enable = false` does not put a
|
||||
# package the machine never built onto a unit's PATH.
|
||||
name = "a host without the runner does not carry it on PATH";
|
||||
ok = !(has noScripting "punktfunk-host" "/pf-stub/punktfunk-scripting/bin");
|
||||
}
|
||||
|
||||
# --- the client half must not drag the host's system wiring in -----------------------------
|
||||
{
|
||||
|
||||
@@ -487,7 +487,15 @@ in
|
||||
]
|
||||
# The HDR-capable gamescope, if enabled. On PATH rather than pinned through
|
||||
# PUNKTFUNK_GAMESCOPE_BIN so an operator's own override of that env still wins.
|
||||
++ optional cfg.host.gamescopeHdr cfg.host.gamescopePackage;
|
||||
++ optional cfg.host.gamescopeHdr cfg.host.gamescopePackage
|
||||
# The plugin runner, if enabled. Package ops (`plugins add`, and the console's store jobs,
|
||||
# which run INSIDE this service) exec `punktfunk-scripting`; its resolution order is
|
||||
# PUNKTFUNK_SCRIPTING -> beside the host binary -> PATH -> the /usr and ~/.local layouts.
|
||||
# On NixOS only the PATH rung can ever match: the runner is a derivation of its OWN, so it
|
||||
# is never beside the host binary and nothing lands in /usr. `environment.systemPackages`
|
||||
# covers an operator's interactive shell but NOT this unit, whose PATH is exactly this
|
||||
# list — without it the console reports a running, enabled runner as "not installed".
|
||||
++ optional cfg.scripting.enable cfg.scripting.package;
|
||||
# Point the host at the WRAPPED encode worker (see `security.wrappers` above). The host's
|
||||
# own resolution order is PUNKTFUNK_ENCODE_WORKER -> alongside /proc/self/exe -> PATH, and
|
||||
# on NixOS the sibling of the store binary is the UNCAPPED store copy — it would run, and
|
||||
|
||||
Reference in New Issue
Block a user