chore(nix): NixOS flake snapshot — host+client packages, module, devShell (WIP)
Local snapshot of the in-flight NixOS support: flake.nix + flake.lock + nix/ (crane host and client packages, services.punktfunk module, devShell). Standalone — nothing in the Rust/Cargo tree references it. Held from push pending its owning session's finalization (Skia-under-Nix follow-up + intended per-workstream PR). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Generated
+64
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1784172867,
|
||||||
|
"narHash": "sha256-hzzeOgTP0MXMvg4djVwysTDvvUXIpsUCgsA0wPMF/Qk=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "8833b7dc3c7426ce1110ed6fed31b6f63d74f2dc",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1784120854,
|
||||||
|
"narHash": "sha256-KesHgItiZPgGX740axSiQLcIQ8D24MDqNpkKYWIek8k=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "753cc8a3a87467296ddd1fa93f0cc3e81120ee46",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1784178955,
|
||||||
|
"narHash": "sha256-YtXHVFBzzqSidcEhPhfTcF2yzzu0p0yA7x07LOR+LaY=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "1785b85aeccca381caf8777133410f577b8f2f59",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
{
|
||||||
|
description = "punktfunk — low-latency desktop/game streaming host + native Linux client";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
crane.url = "github:ipetkov/crane";
|
||||||
|
rust-overlay = {
|
||||||
|
url = "github:oxalica/rust-overlay";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
crane,
|
||||||
|
rust-overlay,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
# Linux/x86_64 only — the host encodes with desktop NVENC and CI publishes no aarch64 leg
|
||||||
|
# (mirrors the RPM's `ExclusiveArch: x86_64`). Add a system here once there's an arm64 build.
|
||||||
|
systems = [ "x86_64-linux" ];
|
||||||
|
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system);
|
||||||
|
|
||||||
|
# The workspace version is the single source of truth (crates/*/Cargo.toml inherit it).
|
||||||
|
version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).workspace.package.version;
|
||||||
|
|
||||||
|
pkgsFor = system: import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ (import rust-overlay) ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Pin cargo/rustc EXACTLY to rust-toolchain.toml (channel 1.96.0 + rustfmt/clippy) so a Nix
|
||||||
|
# build, a dev shell and CI all use the identical toolchain — the repo is deliberate about
|
||||||
|
# this (see rust-toolchain.toml's header on rustfmt format-drift).
|
||||||
|
toolchainFor = pkgs: pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
|
||||||
|
|
||||||
|
craneLibFor = pkgs: (crane.mkLib pkgs).overrideToolchain toolchainFor;
|
||||||
|
|
||||||
|
packagesFor = system:
|
||||||
|
let
|
||||||
|
pkgs = pkgsFor system;
|
||||||
|
in
|
||||||
|
pkgs.callPackage ./nix/packages.nix {
|
||||||
|
craneLib = craneLibFor pkgs;
|
||||||
|
src = self;
|
||||||
|
inherit version;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = forAllSystems (system:
|
||||||
|
let pf = packagesFor system;
|
||||||
|
in {
|
||||||
|
inherit (pf) punktfunk-host punktfunk-client;
|
||||||
|
default = pf.punktfunk-host;
|
||||||
|
});
|
||||||
|
|
||||||
|
# `nix run .#punktfunk-host -- serve` / `nix run .#punktfunk-client`.
|
||||||
|
apps = forAllSystems (system:
|
||||||
|
let pf = packagesFor system;
|
||||||
|
in {
|
||||||
|
punktfunk-host = {
|
||||||
|
type = "app";
|
||||||
|
program = "${pf.punktfunk-host}/bin/punktfunk-host";
|
||||||
|
};
|
||||||
|
punktfunk-client = {
|
||||||
|
type = "app";
|
||||||
|
program = "${pf.punktfunk-client}/bin/punktfunk-client";
|
||||||
|
};
|
||||||
|
default = self.apps.${system}.punktfunk-host;
|
||||||
|
});
|
||||||
|
|
||||||
|
# `nix flake check` builds both packages.
|
||||||
|
checks = forAllSystems (system:
|
||||||
|
let pf = packagesFor system;
|
||||||
|
in {
|
||||||
|
inherit (pf) punktfunk-host punktfunk-client;
|
||||||
|
});
|
||||||
|
|
||||||
|
# `nix develop` — the pinned toolchain plus every system lib the workspace links, wired so
|
||||||
|
# `cargo build` (all crates, host + client) works out of the box.
|
||||||
|
devShells = forAllSystems (system:
|
||||||
|
let
|
||||||
|
pkgs = pkgsFor system;
|
||||||
|
toolchain = toolchainFor pkgs;
|
||||||
|
gbm = pkgs.libgbm or pkgs.mesa;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
strictDeps = false;
|
||||||
|
nativeBuildInputs = [
|
||||||
|
toolchain
|
||||||
|
pkgs.rust-analyzer
|
||||||
|
pkgs.pkg-config
|
||||||
|
pkgs.cmake
|
||||||
|
pkgs.nasm
|
||||||
|
pkgs.perl
|
||||||
|
pkgs.rustPlatform.bindgenHook
|
||||||
|
];
|
||||||
|
buildInputs = [
|
||||||
|
# host
|
||||||
|
pkgs.ffmpeg
|
||||||
|
pkgs.pipewire
|
||||||
|
pkgs.libopus
|
||||||
|
pkgs.wayland
|
||||||
|
pkgs.libxkbcommon
|
||||||
|
pkgs.libGL
|
||||||
|
gbm
|
||||||
|
pkgs.vulkan-loader
|
||||||
|
# client
|
||||||
|
pkgs.sdl3
|
||||||
|
pkgs.gtk4
|
||||||
|
pkgs.libadwaita
|
||||||
|
pkgs.glib
|
||||||
|
pkgs.librsvg
|
||||||
|
pkgs.gsettings-desktop-schemas
|
||||||
|
pkgs.adwaita-icon-theme
|
||||||
|
pkgs.vulkan-headers
|
||||||
|
];
|
||||||
|
# pf-ffvk bindgen (no /usr/include on NixOS); GPU driver libs at runtime for `cargo run`.
|
||||||
|
PF_FFVK_VULKAN_INCLUDE = "${pkgs.vulkan-headers}/include";
|
||||||
|
# CMake ≥ 4 rejects the pre-3.5 minimums some vendored C libs (libopus) still declare.
|
||||||
|
CMAKE_POLICY_VERSION_MINIMUM = "3.5";
|
||||||
|
LD_LIBRARY_PATH = "/run/opengl-driver/lib:${pkgs.lib.makeLibraryPath [ pkgs.vulkan-loader pkgs.libGL gbm ]}";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
formatter = forAllSystems (system: (pkgsFor system).nixfmt-rfc-style);
|
||||||
|
|
||||||
|
# NixOS integration — see nix/nixos-module.nix and nix/README.md.
|
||||||
|
# imports = [ punktfunk.nixosModules.default ];
|
||||||
|
# services.punktfunk.host.enable = true;
|
||||||
|
nixosModules.default = import ./nix/nixos-module.nix self;
|
||||||
|
nixosModules.punktfunk = self.nixosModules.default;
|
||||||
|
};
|
||||||
|
}
|
||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
# punktfunk on NixOS / Nix
|
||||||
|
|
||||||
|
First-class Nix support via the repo's `flake.nix`: reproducible builds of the streaming **host**
|
||||||
|
and the native Linux **client**, a **NixOS module** that wires up everything the RPM/deb do
|
||||||
|
(systemd user service, udev rules, kernel modules, sysctl tuning, firewall, `input` group), and a
|
||||||
|
**dev shell** with the pinned toolchain and every system library.
|
||||||
|
|
||||||
|
> **Platform:** `x86_64-linux` only (the host encodes with desktop NVENC; matches the RPM's
|
||||||
|
> `ExclusiveArch: x86_64`). NixOS **24.11 or newer** for the `hardware.graphics` option.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What the flake provides
|
||||||
|
|
||||||
|
| Output | Contents |
|
||||||
|
| --- | --- |
|
||||||
|
| `packages.x86_64-linux.punktfunk-host` | `punktfunk-host` + `punktfunk-tray` (built with `nvenc` + `vulkan-encode`, like CI) |
|
||||||
|
| `packages.x86_64-linux.punktfunk-client` | `punktfunk-client` (GTK4 shell) + `punktfunk-session` (Vulkan streamer, without the Skia OSD — see caveats) |
|
||||||
|
| `packages.x86_64-linux.default` | = `punktfunk-host` |
|
||||||
|
| `nixosModules.default` | `services.punktfunk.host` / `services.punktfunk.client` |
|
||||||
|
| `devShells.x86_64-linux.default` | pinned Rust (from `rust-toolchain.toml`) + all build deps |
|
||||||
|
| `apps` / `checks` / `formatter` | `nix run`, `nix flake check`, `nix fmt` |
|
||||||
|
|
||||||
|
One binary per GPU vendor: NVENC/CUDA entry points are `dlopen`'d at runtime, so the host runs on
|
||||||
|
NVIDIA (zero-copy dmabuf → CUDA → NVENC), AMD/Intel (raw Vulkan-Video HEVC / VAAPI), or software.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start (no NixOS required)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Build
|
||||||
|
nix build git+https://git.unom.io/unom/punktfunk#punktfunk-host
|
||||||
|
nix build git+https://git.unom.io/unom/punktfunk#punktfunk-client
|
||||||
|
|
||||||
|
# Run
|
||||||
|
nix run git+https://git.unom.io/unom/punktfunk#punktfunk-host -- serve --gamestream
|
||||||
|
nix run git+https://git.unom.io/unom/punktfunk#punktfunk-client
|
||||||
|
```
|
||||||
|
|
||||||
|
GPU drivers are resolved at runtime from `/run/opengl-driver/lib`. On non-NixOS distros use
|
||||||
|
[nixGL](https://github.com/nix-community/nixGL) so that path is populated (`nixGL nix run …`); on
|
||||||
|
NixOS the module (below) sets `hardware.graphics.enable = true` for you.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NixOS module
|
||||||
|
|
||||||
|
Add the flake and enable the host and/or client:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs.punktfunk.url = "git+https://git.unom.io/unom/punktfunk";
|
||||||
|
# (optional) share your nixpkgs: inputs.punktfunk.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, punktfunk, ... }: {
|
||||||
|
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
punktfunk.nixosModules.default
|
||||||
|
({ ... }: {
|
||||||
|
services.punktfunk.host = {
|
||||||
|
enable = true;
|
||||||
|
users = [ "alice" ]; # → added to the `input` group for virtual gamepads
|
||||||
|
openFirewall = true; # native + GameStream ports
|
||||||
|
settings = {
|
||||||
|
PUNKTFUNK_VIDEO_SOURCE = "virtual";
|
||||||
|
RUST_LOG = "info";
|
||||||
|
# PUNKTFUNK_444 = true; # booleans render as 1/0
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# …and/or the client on the same or another box:
|
||||||
|
services.punktfunk.client = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true; # UDP 5353 for mDNS discovery
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, in your graphical session:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
systemctl --user enable --now punktfunk-host
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
`services.punktfunk.host`:
|
||||||
|
|
||||||
|
| Option | Default | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `enable` | `false` | Install the host + wire udev/sysctl/kernel-modules/firewall and the user service. |
|
||||||
|
| `gamestream` | `true` | `serve --gamestream` (Moonlight-compatible). `false` = native-only, more secure. |
|
||||||
|
| `autoStart` | `false` | Add the user service to `default.target` (appliance mode — pair with lingering). |
|
||||||
|
| `users` | `[ ]` | Users added to the `input` group (virtual gamepads). |
|
||||||
|
| `settings` | `{ }` | `host.env` key/values (see `${package}/share/punktfunk-host/host.env.example`). |
|
||||||
|
| `environmentFile` | `null` | Extra `EnvironmentFile` for secrets (e.g. `PUNKTFUNK_MGMT_TOKEN`); loaded optionally. |
|
||||||
|
| `openFirewall` | `false` | Open the inbound ports (see below). |
|
||||||
|
| `package` | flake's | Override the package. |
|
||||||
|
|
||||||
|
`services.punktfunk.client`: `enable`, `openFirewall` (UDP 5353), `package`.
|
||||||
|
|
||||||
|
### What the host module configures for you
|
||||||
|
|
||||||
|
Everything the RPM's `%install` + `%post` do, declaratively:
|
||||||
|
|
||||||
|
- **systemd `--user` service** `punktfunk-host` → `serve [--gamestream]`, `EnvironmentFile` from
|
||||||
|
`settings` (+ optional secret file), `Restart=on-failure`.
|
||||||
|
- **udev rules** (`60-punktfunk.rules`): `/dev/uinput` + `/dev/uhid` group access and the vhci
|
||||||
|
sysfs perms for the virtual Steam Deck.
|
||||||
|
- **kernel modules**: `uinput`, `uhid`, `vhci-hcd` (usbip transport so Steam Input adopts the
|
||||||
|
virtual Deck).
|
||||||
|
- **sysctl**: `net.core.{r,w}mem_max = 32 MB` (high-bitrate UDP headroom; `mkDefault`).
|
||||||
|
- **`input` group** membership for `users`.
|
||||||
|
- **`hardware.graphics.enable = true`** (`mkDefault`) so `/run/opengl-driver/lib` has the driver
|
||||||
|
libs the binaries `dlopen`.
|
||||||
|
- **firewall** (when `openFirewall`): native UDP 9777/5353 + TCP 47990; with `gamestream` also TCP
|
||||||
|
47984/47989/48010 + UDP 47998/47999/48000. The media data plane is an ephemeral, hole-punched
|
||||||
|
UDP port — nothing fixed to open.
|
||||||
|
- **tray autostart** entry (`--autostart`; self-gates to users who actually run a host).
|
||||||
|
|
||||||
|
### GPU drivers (out of scope of the module — set these yourself)
|
||||||
|
|
||||||
|
- **NVIDIA:** `hardware.nvidia` + `hardware.graphics.enable = true`. NVENC/CUDA come from the
|
||||||
|
driver at runtime (nothing pinned in the closure).
|
||||||
|
- **AMD/Intel:** `hardware.graphics.enable = true` with `extraPackages = [ vaapiVdpau … ]` /
|
||||||
|
`intel-media-driver` for VAAPI encode; the host's raw Vulkan-Video HEVC path needs only Mesa.
|
||||||
|
|
||||||
|
### Headless / appliance
|
||||||
|
|
||||||
|
Set `autoStart = true`, enable lingering, and pick a backend in `settings`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.punktfunk.host = {
|
||||||
|
enable = true;
|
||||||
|
autoStart = true;
|
||||||
|
users = [ "streamer" ];
|
||||||
|
settings = { PUNKTFUNK_COMPOSITOR = "gamescope"; }; # or kwin/mutter/wlroots
|
||||||
|
};
|
||||||
|
users.users.streamer.linger = true;
|
||||||
|
# For the gamescope/KWin backends extend the service PATH, e.g.:
|
||||||
|
# systemd.user.services.punktfunk-host.path = [ pkgs.gamescope ];
|
||||||
|
```
|
||||||
|
|
||||||
|
The `${package}/share/punktfunk-host/headless/` helpers (KDE/Sway session scripts, example
|
||||||
|
`host.env` files, the OpenAPI doc) are installed for reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix develop # pinned toolchain (rust-toolchain.toml) + all system libs
|
||||||
|
cargo build --release -p punktfunk-host -p punktfunk-client-linux -p punktfunk-client-session -p punktfunk-tray
|
||||||
|
```
|
||||||
|
|
||||||
|
The shell exports `PF_FFVK_VULKAN_INCLUDE` (Vulkan headers for pf-ffvk bindgen) and an
|
||||||
|
`LD_LIBRARY_PATH` that includes `/run/opengl-driver/lib` so `cargo run` finds the GPU driver.
|
||||||
|
`nix fmt` formats the `.nix` files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes & caveats
|
||||||
|
|
||||||
|
- **Build tool:** [crane](https://github.com/ipetkov/crane). The lockfile carries
|
||||||
|
`windows 0.62.2` from both crates.io *and* a pinned `microsoft/windows-rs` git rev (the Windows
|
||||||
|
client), which `rustPlatform.importCargoLock` can't vendor (colliding `name-version`); crane
|
||||||
|
vendors per-source and fetches the git rev via `builtins.fetchGit` (no output hash to maintain).
|
||||||
|
Those crates are `cfg(windows)`-gated — vendored, never compiled on Linux.
|
||||||
|
- **First build compiles from scratch** (no split dep cache — pyrowave-sys builds a CMake tree in
|
||||||
|
its build.rs that a crane "dummy" source would drop) and has no public binary cache, so expect a
|
||||||
|
long initial build. `nix develop` gives incremental rebuilds.
|
||||||
|
- **Commit `flake.lock`:** it pins the input revisions (nixpkgs / crane / rust-overlay). It is
|
||||||
|
generated on first eval and checked in.
|
||||||
|
- **Session Skia OSD is off under Nix.** `punktfunk-session`'s default `ui` feature draws its
|
||||||
|
on-screen stats/console overlay with `skia-safe`, whose build *downloads* a prebuilt Skia from
|
||||||
|
the rust-skia releases — which Nix's network-less build sandbox forbids, and a from-source Skia
|
||||||
|
build pulls the whole gn/ninja/python toolchain plus network-fetched third-party. The feature is
|
||||||
|
explicitly droppable ("same streaming, stats on stdout only"), so the Nix build compiles the
|
||||||
|
session with `--no-default-features --features pyrowave`. **Everything streams**; only the
|
||||||
|
session binary's *optional* on-glass stats overlay is absent, and the **GTK shell
|
||||||
|
(`punktfunk-client`) is skia-free and fully featured.** Re-adding it means teaching skia-bindings
|
||||||
|
to consume a prebuilt Skia offline (a fixed-output derivation of the rust-skia tarball) or a
|
||||||
|
vendored from-source Skia build — a tracked follow-up.
|
||||||
|
|
||||||
|
## Verified
|
||||||
|
|
||||||
|
Both packages build, install, and run on real Nix hardware (NixOS-equivalent: CachyOS + Nix,
|
||||||
|
RTX 5070 Ti, driver 610). `punktfunk-host --version` and `punktfunk-session` run; the driver
|
||||||
|
RUNPATH (`/run/opengl-driver/lib`) and the GTK GApps wrapper (GSettings schemas + pixbuf loaders)
|
||||||
|
are present. Fixes discovered during that bring-up: `CMAKE_POLICY_VERSION_MINIMUM=3.5` (CMake ≥ 4),
|
||||||
|
system `libopus` (audiopus_sys), and the session Skia note above.
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
# NixOS integration for punktfunk — the declarative equivalent of everything the RPM/deb do in
|
||||||
|
# their %install + %post (packaging/rpm/punktfunk.spec, packaging/debian/build-deb.sh):
|
||||||
|
# the systemd *user* service, the uinput/uhid/vhci udev rules, the vhci-hcd autoload, the 32 MB
|
||||||
|
# UDP socket-buffer sysctls, the firewall openers, and the `input`-group membership for virtual
|
||||||
|
# gamepads.
|
||||||
|
#
|
||||||
|
# Usage (flake):
|
||||||
|
# { inputs.punktfunk.url = "git+https://git.unom.io/unom/punktfunk";
|
||||||
|
# outputs = { punktfunk, nixpkgs, ... }: {
|
||||||
|
# nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
||||||
|
# modules = [ punktfunk.nixosModules.default
|
||||||
|
# { services.punktfunk.host.enable = true;
|
||||||
|
# services.punktfunk.host.users = [ "alice" ]; } ];
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# The host is fundamentally a per-user, in-graphical-session service (it drives the live
|
||||||
|
# compositor, PipeWire and the desktop portals), so it ships as a `systemd.user` unit. Enable it
|
||||||
|
# for a session with `systemctl --user enable --now punktfunk-host` (or set `autoStart = true` for a
|
||||||
|
# headless appliance with `users.users.<u>.linger = true`).
|
||||||
|
self:
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib)
|
||||||
|
mkEnableOption mkOption mkIf mkMerge mkDefault types
|
||||||
|
optional optionals optionalString literalExpression
|
||||||
|
concatStringsSep mapAttrsToList genAttrs;
|
||||||
|
|
||||||
|
cfg = config.services.punktfunk;
|
||||||
|
system = pkgs.stdenv.hostPlatform.system;
|
||||||
|
|
||||||
|
# host.env rendering: booleans → 1/0 (what PUNKTFUNK_* knobs expect), everything else verbatim.
|
||||||
|
renderVal = v:
|
||||||
|
if lib.isBool v then (if v then "1" else "0")
|
||||||
|
else toString v;
|
||||||
|
renderEnv = attrs:
|
||||||
|
concatStringsSep "\n" (mapAttrsToList (k: v: "${k}=${renderVal v}") attrs) + "\n";
|
||||||
|
|
||||||
|
hostSettingsFile = pkgs.writeText "punktfunk-host.env" (renderEnv cfg.host.settings);
|
||||||
|
|
||||||
|
# Native punktfunk/1 ports (control plane + discovery + mgmt API). The media data plane is an
|
||||||
|
# ephemeral per-session UDP port the host hole-punches, so nothing fixed to open (see
|
||||||
|
# packaging/linux/punktfunk.ufw).
|
||||||
|
nativeTCP = [ 47990 ]; # mgmt/library REST API (HTTPS + mTLS)
|
||||||
|
nativeUDP = [ 9777 5353 ]; # QUIC control plane + mDNS
|
||||||
|
# GameStream/Moonlight-compat fixed ports (opt-in with `host.gamestream`).
|
||||||
|
gamestreamTCP = [ 47984 47989 48010 ];
|
||||||
|
gamestreamUDP = [ 47998 47999 48000 ];
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.punktfunk = {
|
||||||
|
host = {
|
||||||
|
enable = mkEnableOption "the punktfunk streaming host (systemd --user service + system wiring)";
|
||||||
|
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = self.packages.${system}.punktfunk-host;
|
||||||
|
defaultText = literalExpression "punktfunk.packages.\${system}.punktfunk-host";
|
||||||
|
description = "The punktfunk-host package (bundles punktfunk-host + punktfunk-tray).";
|
||||||
|
};
|
||||||
|
|
||||||
|
gamestream = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Advertise the GameStream/Moonlight-compatible planes (`serve --gamestream`) so a stock
|
||||||
|
Moonlight client can pair. Set to `false` for a native-only, more secure host (no
|
||||||
|
plain-HTTP pairing / legacy GCM path) and drop the GameStream firewall ports.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
autoStart = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Start the host automatically in every user's graphical session (adds it to the user
|
||||||
|
`default.target`). For a login-less appliance, also enable lingering for the host user
|
||||||
|
(`users.users.<name>.linger = true`) so the user service comes up at boot.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
users = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ ];
|
||||||
|
example = [ "alice" ];
|
||||||
|
description = ''
|
||||||
|
Users to add to the `input` group — required for the virtual gamepads the host creates
|
||||||
|
(`/dev/uinput`, `/dev/uhid`, and the usbip/vhci virtual Steam Deck). The host runs as
|
||||||
|
these users' `systemd --user` service.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = mkOption {
|
||||||
|
type = types.attrsOf (types.oneOf [ types.str types.int types.bool ]);
|
||||||
|
default = { };
|
||||||
|
example = literalExpression ''
|
||||||
|
{
|
||||||
|
PUNKTFUNK_VIDEO_SOURCE = "virtual";
|
||||||
|
PUNKTFUNK_COMPOSITOR = "kwin";
|
||||||
|
PUNKTFUNK_444 = true;
|
||||||
|
RUST_LOG = "info";
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
description = ''
|
||||||
|
`host.env` key/value pairs passed to the service via `EnvironmentFile`. See
|
||||||
|
`''${package}/share/punktfunk-host/host.env.example` for the full surface. Booleans render
|
||||||
|
as `1`/`0`. Leave empty to rely on the host's per-connect auto-detection of the
|
||||||
|
compositor + input backend. Do NOT put secrets here (world-readable in the store) — use
|
||||||
|
`environmentFile` instead.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
environmentFile = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
default = null;
|
||||||
|
example = "/run/secrets/punktfunk-host.env";
|
||||||
|
description = ''
|
||||||
|
Extra `EnvironmentFile` layered AFTER `settings` (its values win). For secrets such as
|
||||||
|
`PUNKTFUNK_MGMT_TOKEN`. Loaded optionally (a missing file does not fail the unit).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
openFirewall = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Open the host's inbound ports. Native punktfunk/1 always: UDP 9777 (QUIC) + 5353 (mDNS),
|
||||||
|
TCP 47990 (mgmt API). With `gamestream = true` also TCP 47984/47989/48010 and UDP
|
||||||
|
47998/47999/48000. The ephemeral media UDP port is hole-punched, so a default-deny
|
||||||
|
firewall still streams (it just adds ~2.5 s at session start).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
client = {
|
||||||
|
enable = mkEnableOption "the native punktfunk Linux client";
|
||||||
|
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = self.packages.${system}.punktfunk-client;
|
||||||
|
defaultText = literalExpression "punktfunk.packages.\${system}.punktfunk-client";
|
||||||
|
description = "The punktfunk-client package (bundles punktfunk-client + punktfunk-session).";
|
||||||
|
};
|
||||||
|
|
||||||
|
openFirewall = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Open UDP 5353 (mDNS) so the client can auto-discover hosts on the LAN.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkMerge [
|
||||||
|
# --- shared: whenever either half is enabled -----------------------------------------------
|
||||||
|
(mkIf (cfg.host.enable || cfg.client.enable) {
|
||||||
|
assertions = [{
|
||||||
|
assertion = system == "x86_64-linux";
|
||||||
|
message = "services.punktfunk is x86_64-linux only (desktop NVENC host; no aarch64 build).";
|
||||||
|
}];
|
||||||
|
# The GPU driver libs the binaries dlopen at runtime (libcuda / libnvidia-encode / libEGL /
|
||||||
|
# the Vulkan ICD) live under /run/opengl-driver/lib — provided by hardware.graphics.
|
||||||
|
hardware.graphics.enable = mkDefault true;
|
||||||
|
# 32 MB UDP socket buffers — without this the kernel clamps the host's SO_SNDBUF / client's
|
||||||
|
# SO_RCVBUF and high-bitrate frames overflow (measured: 4 MB cap = 31.6 % loss at 2 Gbps).
|
||||||
|
boot.kernel.sysctl = {
|
||||||
|
"net.core.wmem_max" = mkDefault 33554432;
|
||||||
|
"net.core.rmem_max" = mkDefault 33554432;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- host ----------------------------------------------------------------------------------
|
||||||
|
(mkIf cfg.host.enable {
|
||||||
|
environment.systemPackages = [ cfg.host.package ];
|
||||||
|
# 60-punktfunk.rules: /dev/uinput + /dev/uhid group access + the vhci sysfs perms.
|
||||||
|
services.udev.packages = [ cfg.host.package ];
|
||||||
|
# The vhci attach/detach rule shells out to chgrp/chmod (coreutils) — put them on udev's PATH.
|
||||||
|
services.udev.path = [ pkgs.coreutils ];
|
||||||
|
# uinput/uhid: the virtual X360 + DualSense nodes. vhci-hcd: the usbip transport that makes
|
||||||
|
# the virtual Steam Deck a real USB device (Steam Input only adopts USB pads).
|
||||||
|
boot.kernelModules = [ "uinput" "uhid" "vhci-hcd" ];
|
||||||
|
|
||||||
|
# `input` group membership for the virtual-gamepad nodes (mirrors the RPM's usermod hint).
|
||||||
|
users.groups.input = { };
|
||||||
|
users.users = genAttrs cfg.host.users (_: { extraGroups = [ "input" ]; });
|
||||||
|
|
||||||
|
# Status-tray autostart entry (self-gating: `--autostart` exits unless this user runs a host).
|
||||||
|
environment.etc."xdg/autostart/io.unom.Punktfunk.Tray.desktop".source =
|
||||||
|
"${cfg.host.package}/etc/xdg/autostart/io.unom.Punktfunk.Tray.desktop";
|
||||||
|
|
||||||
|
networking.firewall = mkIf cfg.host.openFirewall {
|
||||||
|
allowedTCPPorts = nativeTCP ++ optionals cfg.host.gamestream gamestreamTCP;
|
||||||
|
allowedUDPPorts = nativeUDP ++ optionals cfg.host.gamestream gamestreamUDP;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.services.punktfunk-host = {
|
||||||
|
description = "punktfunk GameStream + punktfunk/1 streaming host";
|
||||||
|
documentation = [ "https://git.unom.io/unom/punktfunk" ];
|
||||||
|
# Soft ordering: the host listens immediately and only touches the compositor per session.
|
||||||
|
after = [ "pipewire.service" ];
|
||||||
|
wants = [ "pipewire.service" ];
|
||||||
|
wantedBy = optional cfg.host.autoStart "default.target";
|
||||||
|
# The host may exec external helpers (pw-dump, sh, and — for the gamescope/kwin backends —
|
||||||
|
# the compositor). Extend this in your config for a headless gamescope/KWin appliance.
|
||||||
|
path = [ pkgs.bash pkgs.coreutils pkgs.pipewire ];
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = "${cfg.host.package}/bin/punktfunk-host serve"
|
||||||
|
+ optionalString cfg.host.gamestream " --gamestream";
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = 2;
|
||||||
|
EnvironmentFile =
|
||||||
|
(optional (cfg.host.settings != { }) "${hostSettingsFile}")
|
||||||
|
++ (optional (cfg.host.environmentFile != null) "-${toString cfg.host.environmentFile}");
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- client --------------------------------------------------------------------------------
|
||||||
|
(mkIf cfg.client.enable {
|
||||||
|
environment.systemPackages = [ cfg.client.package ];
|
||||||
|
# 70-punktfunk-client.rules: hidraw access for the seated user's DualSense (SDL HIDAPI). The
|
||||||
|
# rule is uaccess-tagged, so the active-seat user gets it with no group membership.
|
||||||
|
services.udev.packages = [ cfg.client.package ];
|
||||||
|
|
||||||
|
networking.firewall = mkIf cfg.client.openFirewall {
|
||||||
|
allowedUDPPorts = [ 5353 ];
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
# punktfunk Nix packages — built with crane (github:ipetkov/crane).
|
||||||
|
#
|
||||||
|
# Two derivations, mirroring the RPM's `punktfunk` / `punktfunk-client` split (packaging/rpm/
|
||||||
|
# punktfunk.spec) so a headless streaming host never drags in the GTK client closure:
|
||||||
|
#
|
||||||
|
# * punktfunk-host — the streaming host + status tray (punktfunk-host, punktfunk-tray).
|
||||||
|
# Built with the same feature set CI ships: the direct-SDK NVENC path
|
||||||
|
# (`nvenc`) + the raw Vulkan-Video HEVC encoder (`vulkan-encode`). Both are
|
||||||
|
# AMD/Intel-safe (NVENC/CUDA entry points are dlopen'd at RUNTIME, never
|
||||||
|
# link-imported), so ONE binary runs on NVIDIA, AMD and Intel.
|
||||||
|
# * punktfunk-client — the native Linux client: the GTK4/libadwaita shell (punktfunk-client) plus
|
||||||
|
# the Vulkan/ash session streamer it execs (punktfunk-session).
|
||||||
|
#
|
||||||
|
# Why crane and not rustPlatform.buildRustPackage:
|
||||||
|
# * The lockfile carries `windows 0.62.2` (and friends) from BOTH crates.io AND a pinned
|
||||||
|
# microsoft/windows-rs git rev — the Windows client uses the git one. importCargoLock keys its
|
||||||
|
# vendor dir by `name-version`, so those same-name-same-version-different-source entries collide;
|
||||||
|
# crane vendors per-source and handles it. (Those crates are `cfg(windows)`-gated and never
|
||||||
|
# COMPILE on Linux — they only need to be vendored to satisfy the lock.)
|
||||||
|
# * crane fetches git deps with `builtins.fetchGit` (the rev is a full sha ⇒ pure-eval-safe), so
|
||||||
|
# no hand-maintained `outputHashes` for the windows-rs checkout.
|
||||||
|
#
|
||||||
|
# The whole workspace is built from real source (no crane dep-only "dummy" pre-build): pyrowave-sys
|
||||||
|
# runs CMake over its committed `vendor/pyrowave` tree in build.rs, which a dummy src would omit —
|
||||||
|
# so `cargoArtifacts = null` keeps deps + crate in one derivation.
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
craneLib,
|
||||||
|
src,
|
||||||
|
version,
|
||||||
|
# native tooling
|
||||||
|
pkg-config,
|
||||||
|
cmake,
|
||||||
|
nasm,
|
||||||
|
perl,
|
||||||
|
rustPlatform,
|
||||||
|
addDriverRunpath,
|
||||||
|
wrapGAppsHook4,
|
||||||
|
# host + client shared libs
|
||||||
|
ffmpeg,
|
||||||
|
pipewire,
|
||||||
|
libopus,
|
||||||
|
wayland,
|
||||||
|
libxkbcommon,
|
||||||
|
libGL,
|
||||||
|
vulkan-loader,
|
||||||
|
# gbm lives in `libgbm` on recent nixpkgs, `mesa` on older ones.
|
||||||
|
libgbm ? null,
|
||||||
|
mesa,
|
||||||
|
# client-only libs
|
||||||
|
sdl3,
|
||||||
|
gtk4,
|
||||||
|
libadwaita,
|
||||||
|
glib,
|
||||||
|
librsvg,
|
||||||
|
gsettings-desktop-schemas,
|
||||||
|
adwaita-icon-theme,
|
||||||
|
vulkan-headers,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
gbm = if libgbm != null then libgbm else mesa;
|
||||||
|
|
||||||
|
# Build provenance stamped into the binary (build.rs reads PUNKTFUNK_BUILD_VERSION for
|
||||||
|
# `--version` / mgmt /health), mirroring the deb/rpm `~ci`-style stamp.
|
||||||
|
buildVersion = "${version}-nix";
|
||||||
|
|
||||||
|
# `cargo build --release --locked` scoped to the crates each package ships. crane appends this to
|
||||||
|
# both the vendor step and the compile.
|
||||||
|
commonArgs = {
|
||||||
|
inherit src version;
|
||||||
|
strictDeps = true;
|
||||||
|
|
||||||
|
# nixpkgs ships CMake ≥ 4, which errors on `cmake_minimum_required(VERSION <3.5)`. Several
|
||||||
|
# vendored C libraries built through the `cmake` crate still declare a pre-3.5 minimum
|
||||||
|
# (audiopus_sys' libopus; belt-and-braces for pyrowave-sys / aws-lc-sys). CMake reads this env
|
||||||
|
# var as the floor policy version, letting those configure instead of aborting.
|
||||||
|
CMAKE_POLICY_VERSION_MINIMUM = "3.5";
|
||||||
|
|
||||||
|
# crane's default is a two-derivation split (buildDepsOnly → buildPackage); disable it, because
|
||||||
|
# the deps-only pass builds a "dummy" source tree that drops pyrowave-sys's `vendor/pyrowave`
|
||||||
|
# CMake inputs and would fail its build.rs. One derivation compiles everything from real src.
|
||||||
|
cargoArtifacts = null;
|
||||||
|
|
||||||
|
# The workspace has hardware-bound tests (uinput, a real GPU); packaging builds never run them,
|
||||||
|
# exactly like the deb/rpm/PKGBUILD paths.
|
||||||
|
doCheck = false;
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkg-config
|
||||||
|
cmake # pyrowave-sys (C++/Vulkan), the vendored libopus (opus crate), aws-lc-sys (rustls)
|
||||||
|
nasm # libopus SIMD + OpenH264 (openh264 `source` feature)
|
||||||
|
perl # aws-lc-sys asm generation (rustls' aws-lc-rs crypto provider)
|
||||||
|
rustPlatform.bindgenHook # LIBCLANG_PATH + clang args for ffmpeg-sys-next / pf-ffvk / pyrowave-sys bindgen
|
||||||
|
addDriverRunpath # provides the `addDriverRunpath` shell fn used in postFixup
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
homepage = "https://git.unom.io/unom/punktfunk";
|
||||||
|
license = with lib.licenses; [ mit asl20 ];
|
||||||
|
platforms = [ "x86_64-linux" ]; # NVENC desktop host; matches the RPM's ExclusiveArch: x86_64
|
||||||
|
maintainers = [ ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
punktfunk-host = craneLib.buildPackage (commonArgs // {
|
||||||
|
pname = "punktfunk-host";
|
||||||
|
cargoExtraArgs =
|
||||||
|
"--locked -p punktfunk-host -p punktfunk-tray "
|
||||||
|
+ "--features punktfunk-host/nvenc,punktfunk-host/vulkan-encode";
|
||||||
|
|
||||||
|
PUNKTFUNK_BUILD_VERSION = buildVersion;
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
ffmpeg # libavcodec/avformat/avutil (NVENC + VAAPI encode), via ffmpeg-next → ffmpeg-sys-next pkg-config
|
||||||
|
pipewire # libpipewire-0.3 + libspa-0.2 (portal capture + the `pipewire` crate)
|
||||||
|
libopus # audiopus_sys links system opus via pkg-config (else it vendors a static libopus that mis-links)
|
||||||
|
wayland # libwayland-client (wlr / KWin fake_input backends)
|
||||||
|
libxkbcommon # virtual-keyboard keymap build/validate
|
||||||
|
libGL # src/linux/zerocopy/egl.rs `#[link(name = "GL")]`
|
||||||
|
gbm # src/linux/zerocopy/egl.rs `#[link(name = "gbm")]`
|
||||||
|
vulkan-loader # ash (Vulkan-Video encode + the dmabuf→VK bridge); loaded at runtime
|
||||||
|
];
|
||||||
|
|
||||||
|
# KWin resolves a streaming client to a .desktop by matching /proc/<pid>/exe against `Exec`, so
|
||||||
|
# the host's authorization .desktop must name the ACTUAL binary path (the store path here). The
|
||||||
|
# tray/client .desktop Exec lines are rewritten for the same reason (menu launch / autostart).
|
||||||
|
postInstall = ''
|
||||||
|
# udev: /dev/uinput + /dev/uhid (virtual gamepads) + the vhci sysfs perms for the virtual Deck.
|
||||||
|
install -Dm0644 scripts/60-punktfunk.rules "$out/lib/udev/rules.d/60-punktfunk.rules"
|
||||||
|
|
||||||
|
# KWin Desktop-mode authorization (zkde_screencast + fake_input). Point Exec at the store binary.
|
||||||
|
install -Dm0644 packaging/linux/io.unom.Punktfunk.Host.desktop \
|
||||||
|
"$out/share/applications/io.unom.Punktfunk.Host.desktop"
|
||||||
|
substituteInPlace "$out/share/applications/io.unom.Punktfunk.Host.desktop" \
|
||||||
|
--replace-fail "/usr/bin/punktfunk-host" "$out/bin/punktfunk-host"
|
||||||
|
|
||||||
|
# Status-tray autostart entry + its hicolor status icons.
|
||||||
|
install -Dm0644 packaging/linux/io.unom.Punktfunk.Tray.desktop \
|
||||||
|
"$out/etc/xdg/autostart/io.unom.Punktfunk.Tray.desktop"
|
||||||
|
substituteInPlace "$out/etc/xdg/autostart/io.unom.Punktfunk.Tray.desktop" \
|
||||||
|
--replace-fail "/usr/bin/punktfunk-tray" "$out/bin/punktfunk-tray"
|
||||||
|
for sz in 22x22 48x48; do
|
||||||
|
for png in packaging/linux/icons/hicolor/$sz/apps/*.png; do
|
||||||
|
install -Dm0644 "$png" "$out/share/icons/hicolor/$sz/apps/$(basename "$png")"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Reference material: headless session helpers, example env files, the OpenAPI doc.
|
||||||
|
install -Dm0755 scripts/headless/run-headless-kde.sh "$out/share/punktfunk-host/headless/run-headless-kde.sh"
|
||||||
|
install -Dm0755 scripts/headless/run-headless-sway.sh "$out/share/punktfunk-host/headless/run-headless-sway.sh"
|
||||||
|
install -Dm0644 scripts/headless/kde-authorized "$out/share/punktfunk-host/headless/kde-authorized"
|
||||||
|
install -Dm0644 scripts/headless/punktfunk-sink.conf "$out/share/punktfunk-host/headless/punktfunk-sink.conf"
|
||||||
|
install -Dm0644 scripts/host.env.example "$out/share/punktfunk-host/host.env.example"
|
||||||
|
install -Dm0644 packaging/bazzite/host.env "$out/share/punktfunk-host/host.env.bazzite"
|
||||||
|
install -Dm0644 packaging/kde/host.env "$out/share/punktfunk-host/host.env.kde"
|
||||||
|
install -Dm0644 api/openapi.json "$out/share/punktfunk-host/openapi.json"
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Run AFTER fixup (patchelf --shrink-rpath would otherwise drop /run/opengl-driver/lib, which is
|
||||||
|
# empty at build time): append the driver runpath so the runtime dlopen of libcuda.so.1 /
|
||||||
|
# libnvidia-encode.so.1 / libEGL.so.1 / the GPU's libvulkan ICD resolves from the running system.
|
||||||
|
postFixup = ''
|
||||||
|
addDriverRunpath "$out/bin/punktfunk-host" "$out/bin/punktfunk-tray"
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = meta // {
|
||||||
|
description = "Low-latency desktop/game streaming host (Moonlight-compatible + native punktfunk/1)";
|
||||||
|
mainProgram = "punktfunk-host";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
punktfunk-client = craneLib.buildPackage (commonArgs // {
|
||||||
|
pname = "punktfunk-client";
|
||||||
|
# The session's default `ui` feature pulls skia-safe, whose build DOWNLOADS a prebuilt Skia
|
||||||
|
# (rust-skia releases) — impossible in Nix's network-less build sandbox, and a from-source Skia
|
||||||
|
# build needs the full gn/ninja/python toolchain + network-fetched third-party. The `ui` feature
|
||||||
|
# is explicitly droppable (clients/session/Cargo.toml: "same streaming, stats on stdout only"),
|
||||||
|
# so build the session without it. The GTK shell (punktfunk-client-linux) is skia-free and full.
|
||||||
|
# Re-adding the Skia OSD under Nix is tracked in nix/README.md.
|
||||||
|
cargoExtraArgs =
|
||||||
|
"--locked -p punktfunk-client-linux -p punktfunk-client-session "
|
||||||
|
+ "--no-default-features --features punktfunk-client-session/pyrowave";
|
||||||
|
|
||||||
|
# pf-ffvk runs bindgen over libavutil/hwcontext_vulkan.h, which `#include <vulkan/vulkan.h>`.
|
||||||
|
# There is no /usr/include on NixOS, so hand it the Vulkan-Headers include dir explicitly
|
||||||
|
# (build.rs turns this into a clang `-I`). libavutil's own include path comes from pkg-config.
|
||||||
|
PF_FFVK_VULKAN_INCLUDE = "${vulkan-headers}/include";
|
||||||
|
|
||||||
|
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [ wrapGAppsHook4 ];
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
ffmpeg # FFmpeg decode (pf-client-core) + pf-ffvk links libavutil
|
||||||
|
pipewire # PipeWire audio playback + mic capture
|
||||||
|
libopus # audiopus_sys → system opus via pkg-config (Opus decode)
|
||||||
|
sdl3 # window + gamepads (SDL3 HIDAPI: DualSense touchpad/motion/triggers)
|
||||||
|
gtk4 # the GTK4 shell (clients/linux, relm4)
|
||||||
|
libadwaita
|
||||||
|
glib
|
||||||
|
librsvg # gdk-pixbuf SVG loader for symbolic icons
|
||||||
|
gsettings-desktop-schemas
|
||||||
|
adwaita-icon-theme
|
||||||
|
libxkbcommon
|
||||||
|
libGL
|
||||||
|
vulkan-loader # ash presenter + Vulkan-Video decode (loaded at runtime)
|
||||||
|
];
|
||||||
|
|
||||||
|
# wrapGApp needs to run on the real ELF, and addDriverRunpath must run post-shrink — so disable
|
||||||
|
# the hook's blanket auto-wrap and drive both by hand in postFixup (see the host note above).
|
||||||
|
dontWrapGApps = true;
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
# hidraw access for the seated user's DualSense (SDL HIDAPI full-fidelity path).
|
||||||
|
install -Dm0644 scripts/70-punktfunk-client.rules "$out/lib/udev/rules.d/70-punktfunk-client.rules"
|
||||||
|
|
||||||
|
# Desktop launcher — point Exec at the store binary (wrapped below).
|
||||||
|
install -Dm0644 packaging/linux/io.unom.Punktfunk.desktop \
|
||||||
|
"$out/share/applications/io.unom.Punktfunk.desktop"
|
||||||
|
substituteInPlace "$out/share/applications/io.unom.Punktfunk.desktop" \
|
||||||
|
--replace-fail "Exec=punktfunk-client" "Exec=$out/bin/punktfunk-client"
|
||||||
|
'';
|
||||||
|
|
||||||
|
postFixup = ''
|
||||||
|
addDriverRunpath "$out/bin/punktfunk-client" "$out/bin/punktfunk-session"
|
||||||
|
# Only the GTK shell needs the GApps wrapper (GSETTINGS_SCHEMA_DIR, icon themes, typelibs);
|
||||||
|
# the ash session binary is not a GTK app.
|
||||||
|
wrapGApp "$out/bin/punktfunk-client"
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = meta // {
|
||||||
|
description = "Native Linux punktfunk/1 streaming client (GTK4 shell + Vulkan session)";
|
||||||
|
mainProgram = "punktfunk-client";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user