Compare commits

...
4 Commits
Author SHA1 Message Date
mollusk 0e395e5c0c build(nix): pin the Rust toolchain to 1.97.1 via rust-overlay
CI / check (push) Waiting to run
nixpkgs 26.05 ships rustc 1.95.0, but this crate was developed and verified on
1.97.1 (what CachyOS had, installed 2026-07-17). Taking the compiler from
oxalica/rust-overlay decouples "which Rust the project targets" from "which
release the audio stack came from", so a nixpkgs bump can no longer move the
compiler under the lint gate as a side effect.

Chosen over rustup, which would also have worked here (nix-ld is enabled, so
its prebuilt binaries run) and would have let one rust-toolchain.toml cover the
packaging distroboxes too. The deciding factor is purity: rustup records
nothing in flake.lock, so a fresh clone or darp5 would resolve whatever it
fetched that day. rust-overlay gives the same exact-version control with the
choice pinned in the lock.

`.default` is the rustup "default" profile — rustc, cargo, rust-std, rustfmt
and clippy — so those are no longer listed individually. rust-src and the
x86_64-pc-windows-gnu target are added for win-cross-build.sh, which needs
`-Z build-std=std,panic_abort`; that script still expects the peerspeak-win
distrobox for the mingw half.

Verified on 1.97.1: 640 lib tests pass, fmt clean.

NOT fixed here, and pre-existing rather than a migration artifact:
`cargo clippy --all-targets -- -D warnings` fails with 15 warnings — 13
`float_literal_f32_fallback` (bare 0.05/0.01 into `.step()`, wants `0.05_f32`),
one `manual implementation of Option::filter`, one `redundant reference in
format!`, all in src/app/mod.rs. The f32 lint is `future_incompatible` and is
slated to become a hard error, so it needs fixing regardless of platform.
CachyOS was already on 1.97.1 well before the 2026-07-31 S2 merge logged as
"clippy clean", so that claim reflects a plain `cargo clippy` run, which exits
0 on warnings. `cargo clippy --fix` applies all 15 automatically.
2026-08-07 14:03:35 -04:00
mollusk 774922c6a9 build(nix): add a devShell so peerspeak builds on NixOS
CI / check (push) Waiting to run
The repo assumed a distro with a system-wide Rust, which NixOS does not
provide. This adds a flake devShell carrying the full dependency surface:

- Build: rustc/cargo/clippy/rustfmt, pkg-config, clang (pipewire-sys and
  libspa-sys need a real libclang for bindgen, via LIBCLANG_PATH), cmake
  (audiopus_sys's vendored-libopus fallback), and git (build.rs stamps
  PEERSPEAK_GIT_SHORT from `git rev-parse`).
- Link: alsa-lib, libopus, pipewire.
- dlopen'd at runtime: vulkan-loader, libxkbcommon, wayland and the X11 libs.
  Nothing links these, so they never land in the binary's rpath and are
  reachable only through LD_LIBRARY_PATH. Omitting them builds fine and then
  fails at window creation, which is a confusing way to find out.
- The supply-chain gates CI runs (cargo-deny, cargo-audit) plus cargo-deb.
  These were `cargo install`ed on the CachyOS side, which does not carry
  over — those binaries link that distro's glibc.

It also carries the screen-share tools (GStreamer + plugin search path,
pactl, mpv). Those look like they belong only to pixelpass, but
tests/screenshare_host_fault.rs starts a REAL pixelpass host, which aborts at
its own preflight without them — so they are a dependency of this test suite.
They are duplicated from pixelpass's flake rather than imported: the two
projects are mutually optional by design, and having one flake consume the
other would reintroduce the build-level dependency that rule prevents.

nixpkgs is pinned to nixos-26.05, the same channel the hosts run, so the
libraries here match the running PipeWire daemon and Vulkan ICD.

Verified: 640 lib tests pass, all 4 screenshare_host_fault live gates pass
(real audio backend, real network bind, real pixelpass child), fmt clean.
Known delta: clippy 1.95.0 (nixpkgs 26.05) flags one collapsible_match in
src/widget/selectable_text.rs that clippy 1.97.1 on CachyOS did not.
2026-08-07 13:46:18 -04:00
molluskandClaude Opus 5 63c246d976 docs: record the jitter buffer's unreachable shrink path as a known bug
CI / check (push) Waiting to run
`target_delay` grows +1 per disruption to MAX_DELAY_FRAMES (240 ms) but only
shrinks after 250 consecutive clean frames — 5 s of unbroken audio. Two of the
five `clean_run` resets fire on every natural pause in speech (jitter.rs:201
benign underrun, jitter.rs:181 re-prime), and the sender stops transmitting
outright while the gate is closed (core/mod.rs:2041). The AIMD decrease half is
therefore unreachable under conversational voice: one early jitter burst pins
the extra latency for the rest of the session.

Found by code review; not yet reproduced live. Pairs with field-test debt #5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:38:26 -04:00
molluskandClaude Fable 5 52d842160d Merge s2-host-fault: the screenshare host-fault path (S2)
A pixelpass host that dies mid-share is now torn down instead of
staying advertised: stdout EOF is synthesized as a terminal fault,
routed back into the core on a dedicated channel behind the reliable
arm of the biased select, gated by the ActiveShare generation so a
reaped child's late EOF is dropped as stale, and handled by retiring
the share — presence ticket removal and ScreenShareStopped ahead of
the reap wait, the explanatory error after.

Reviewed by Gemini (three rounds: branch review, full-range merge
review, fix verification round). Its P2s — Join's early-exit ordering
hole, the reap-then-presence advertising window, and the missing
presence-side gate — are fixed and mutation-verified. 640 lib tests;
three live gates green on the desktop, including a two-process
observer gate that reads the sharer's presence from a second real
node.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 14:53:02 -04:00
4 changed files with 254 additions and 0 deletions
+6
View File
@@ -11,3 +11,9 @@
# the .iss script and .ico are the tracked sources.
/packaging/windows/peerspeak.exe
/packaging/windows/output/
# Nix: the symlink `nix build` drops, and direnv's local cache. flake.nix and
# flake.lock ARE tracked — the lock is what pins the toolchain.
/result
/result-*
/.direnv/
+29
View File
@@ -137,6 +137,35 @@ covers internals). When you ship a feature, add it here.
---
## Known bugs
Defects found by code review, not yet fixed.
1. **Adaptive playout delay never shrinks back in real conversation**
(`src/core/jitter.rs`) — `target_delay` grows +1 per disruption up to
`MAX_DELAY_FRAMES` (12 frames = 240 ms) but only shrinks after
`CLEAN_RUN_TO_SHRINK` = 250 consecutive cleanly-played frames, i.e. **5 s of
unbroken audio**. `clean_run` is reset in five places; two of them fire on
every natural pause in speech: the benign-underrun branch (`jitter.rs:201`,
talker went quiet) and the subsequent re-prime (`jitter.rs:181`). Because the
sender skips transmitting entirely while the noise gate is closed
(`src/core/mod.rs:2041`), a pause between sentences *always* underruns the
receiver and zeroes the clean run — twice.
Net effect: the controller is a one-way ratchet. A single burst of jitter
early in a call pins up to 240 ms of extra playout latency for the rest of
the session, because no conversational speaker talks for 5 continuous
seconds without the gate closing. The AIMD "decrease" half is effectively
unreachable under the workload the app is built for.
Likely fix: let `clean_run` survive a benign idle→re-prime transition rather
than resetting it. Silence is not evidence the link is bad, so it should not
count against the clean run. Distinguish "talker stopped" (benign) from
"playout broke" (real) at `jitter.rs:195-202`.
Found 2026-07-31 by code review. Not yet reproduced in a live call — pairs
with field-test debt item 5 below.
## Known field-test debt (the 🧪 rows above, collected)
Re-run on a real desktop ↔ dopedart call before calling these done:
Generated
+48
View File
@@ -0,0 +1,48 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1785989512,
"narHash": "sha256-HFQhkQcl5D1hUNoen3SGHCSFCt2Bg6uP+HgbrnA3InQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "445d861c6d31b4af0c79d8d4be2331f762a361d7",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-26.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1786076960,
"narHash": "sha256-jfR6OhwurCKn1tREyfOcK/Omxf1Q/DzDDFbnEr1mBLs=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "57a23bfaf4f7017267294b161175db1e32eb1c85",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+171
View File
@@ -0,0 +1,171 @@
{
description = "PeerSpeak decentralized P2P voice chat (Rust/iroh/PipeWire/Opus/iced)";
inputs = {
# Pinned to the same channel the hosts run (nixos-config tracks
# nixos-26.05), so the libraries this shell links and dlopens are built
# against the same release as the PipeWire daemon and Vulkan ICD actually
# running on the machine. Floating to unstable here would reintroduce
# precisely the client/server version skew the pin exists to prevent.
nixpkgs.url = "github:nixos/nixpkgs/nixos-26.05";
# The Rust toolchain is pinned SEPARATELY from the system libraries, and
# deliberately so. nixpkgs 26.05 ships rustc 1.95.0, but this crate was
# developed and verified against 1.97.1 — close enough to build and pass
# every test, but not close enough for clippy, which flags a
# `collapsible_match` on 1.95 that 1.97 does not. Taking the compiler from
# here decouples "which Rust the project targets" from "which release the
# audio stack came from", so a nixpkgs bump can never silently move the
# compiler under the lint gate again.
#
# This is the reproducible alternative to rustup: same exact-version
# control, but the choice is recorded in flake.lock, so darp5 or a fresh
# clone resolves the identical toolchain instead of whatever rustup happens
# to fetch that day.
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{ nixpkgs, rust-overlay, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ rust-overlay.overlays.default ];
};
# Matches what CachyOS shipped (rust 1:1.97.1-1), which is the toolchain
# every green result in the handoff was produced with.
#
# `default` is the rustup "default" profile — rustc, cargo, rust-std,
# rustfmt and clippy — so those are NOT listed separately below.
#
# rust-src and the windows-gnu target exist for win-cross-build.sh, which
# needs `-Z build-std=std,panic_abort` for the self-contained .exe. That
# script still expects to run in the peerspeak-win distrobox for the
# mingw toolchain; carrying the target here just means the Rust half is
# already in place if it is ever driven from the host.
rustToolchain = pkgs.rust-bin.stable."1.97.1".default.override {
extensions = [ "rust-src" ];
targets = [ "x86_64-pc-windows-gnu" ];
};
# Libraries that iced/winit/wgpu open with dlopen at RUNTIME rather than
# linking at build time. Because nothing links them, they never land in
# the binary's rpath — under `cargo run` the loader finds them only
# through LD_LIBRARY_PATH. Leaving them out builds fine and then panics
# at window creation, which is a genuinely confusing failure, so they are
# listed explicitly instead of discovered the hard way.
runtimeLibs = with pkgs; [
vulkan-loader # wgpu's Vulkan backend (iced's renderer)
libxkbcommon # winit keyboard handling
wayland # wayland-sys, dlopen'd on a Wayland session
libx11 # x11-dl, dlopen'd on the X11 fallback path
libxcursor
libxrandr
libxi
];
# Screen sharing spawns pixelpass as a CHILD PROCESS, and pixelpass in
# turn drives GStreamer as a subprocess. That makes these tools a
# dependency of peerspeak's own test suite, not just of pixelpass:
# `tests/screenshare_host_fault.rs` starts a real pixelpass host, which
# aborts at its preflight if gst-launch-1.0 is missing.
#
# Deliberately duplicated from pixelpass's flake rather than importing it
# as an input. The two projects are mutually optional by design — neither
# is a dependency of the other, and the coupling is a runtime subprocess
# contract. Making one flake consume the other would quietly reintroduce
# exactly the build-level dependency that rule exists to prevent.
screenshareTools = with pkgs; [
gst_all_1.gstreamer
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-bad
gst_all_1.gst-plugins-ugly
gst_all_1.gst-libav
pipewire # pipewiresrc (Wayland capture; ships in this pkg)
];
in
{
devShells.${system}.default = pkgs.mkShell {
nativeBuildInputs = [
rustToolchain
]
++ (with pkgs; [
# The supply-chain gates .gitea/workflows/ci.yml runs, so the same
# checks are reproducible locally before a push. These were `cargo
# install`ed on the CachyOS side, which does not carry over — those
# binaries link that distro's glibc and will not run here.
# cargo-deny reads deny.toml; cargo-audit reads .cargo/audit.toml.
cargo-audit
cargo-deny
# Debian packaging (`cargo deb --no-build`). Note the .deb itself
# should still be built inside a Debian/Ubuntu distrobox so the
# binary links that distro's glibc — see the packaging notes in
# Cargo.toml.
cargo-deb
pkg-config
# pipewire-sys and libspa-sys generate their bindings with bindgen,
# which needs a real libclang present at build time.
clang
# audiopus_sys prefers the system libopus via pkg-config but falls
# back to a vendored CMake build; cmake keeps that fallback working
# rather than failing obscurely inside a build script.
cmake
# build.rs shells out to `git rev-parse --short=8 HEAD` to stamp
# PEERSPEAK_GIT_SHORT into the binary (surfaced in Settings).
git
])
++ screenshareTools
++ [
pkgs.pulseaudio # `pactl`, used by pixelpass's audio routing
pkgs.mpv # the screen-share viewer
];
buildInputs =
with pkgs;
[
alsa-lib # alsa-sys, pulled in by rodio/cpal
libopus # audiopus_sys, linked dynamically
pipewire # pipewire-sys + libspa-sys: the Linux audio backend
]
++ runtimeLibs;
# bindgen finds libclang through this variable specifically — having
# clang on PATH is not sufficient.
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath runtimeLibs;
# NixOS keeps every GStreamer plugin in its own store path, so the
# gst-launch-1.0 that pixelpass spawns discovers them ONLY through this
# search path. Same reasoning as hosts/darp5 and hosts/cazen in
# nixos-config.
GST_PLUGIN_SYSTEM_PATH_1_0 =
pkgs.lib.makeSearchPathOutput "lib" "lib/gstreamer-1.0" screenshareTools;
# Only greet an interactive shell. shellHook also runs under
# `nix develop --command …`, where printing this would interleave the
# banner with the command's own output.
shellHook = ''
if [ -t 1 ]; then
echo "peerspeak rustc $(rustc --version | cut -d' ' -f2) / cargo $(cargo --version | cut -d' ' -f2)"
echo " cargo build --release build"
echo " cargo test lib tests"
echo " cargo clippy --all-targets -- -D warnings lint"
echo
echo "Screen sharing spawns pixelpass as a child process it must be"
echo "on PATH. Build it from ../pixelpass and add its target/release."
fi
'';
};
};
}