Author SHA1 Message Date
mollusk 911f0521f8 build(nix): pin the Rust toolchain to 1.97.1 via rust-overlay
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. No windows-gnu target
here: pixelpass is Linux-only, unlike peerspeak.

Verified on 1.97.1: 256 tests pass, fmt clean, and `cargo clippy --all-targets
-- -D warnings` is clean.
2026-08-07 14:03:25 -04:00
mollusk 9ad55c19de build(nix): add a devShell so pixelpass builds on NixOS
The repo assumed a distro with a system-wide Rust and system-wide GStreamer,
which is exactly what NixOS does not provide. This adds a flake devShell
carrying the whole dependency surface:

- Build: rustc/cargo/clippy/rustfmt, pkg-config, and clang — pipewire-sys,
  libspa-sys and libpulse-sys all generate bindings with bindgen, which needs
  a real libclang via LIBCLANG_PATH rather than just clang on PATH.
- Link: pipewire, libpulseaudio, and libxcb. The libxcb one is not obvious:
  x11rb is declared `default-features = false` here, but Cargo unifies
  features across the graph and arboard pulls x11rb with `libxcb` on, so the
  final link really does need -lxcb.
- Runtime: GStreamer is driven as a SUBPROCESS, not linked, so the tools and
  their plugin search path are provided here too. NixOS keeps every plugin in
  its own store path, so gst-launch-1.0 finds them only through
  GST_PLUGIN_SYSTEM_PATH_1_0 — without it the `gst-inspect-1.0 --exists
  pipewiresrc` preflight fails even with the plugins installed.

nixpkgs is pinned to nixos-26.05, the same channel the hosts run, so the
client libraries match the PipeWire daemon and PulseAudio server they talk to.

Verified: 256 tests pass, clippy clean, and `--doctor` reports all checks
passing (capture, encode, mux/audio, viewer, relay).
2026-08-07 13:46:05 -04:00
molluskandClaude Opus 5 347462cca7 Merge 0c step 1: --repair learns ownership, and stops parsing pactl
Nine commits, seven adversarial review rounds. The starting point was a real
defect — after 0c the capture sink is connection-owned, so a dead host leaves
loopbacks with no `module-null-sink` to trace its pid from, and discovery went
blind rather than getting smaller. Everything after that was the review finding
that the fix's foundations were softer than they looked.

What landed:

- Discovery derives candidate pids independently from all three module shapes,
  A/B-proven on the live graph against the old binary.
- Recognition is exact-form only, and the matcher's templates are generated from
  the loader's own renderer, so the two cannot drift; anything naming our sinks
  that matches no known form is reported rather than silently ignored.
- Observation and unloading go through libpulse introspection over one
  verified-local connection. `pactl`'s text output cannot carry this: a genuine
  module whose argument contains a newline renders a first line that is
  byte-exactly canonical (field-confirmed, no adversary needed), the JSON listing
  carries no module index at all, and `PULSE_SERVER` is a fallback list that never
  proved locality.
- A pid is not an owner. Every module carries a machine/boot/pid-namespace token,
  and repair asks about a pid only when all three match — otherwise the module is
  reported and its pid is never even looked up. Untagged modules from older builds
  are refused by default, behind `--repair-legacy-untagged`.
- A plan is not a licence, and neither is ordering: fingerprints are re-verified
  against a fresh snapshot per action, the sink unload is gated on nothing still
  referencing it, and liveness runs before the snapshot so a replacement arriving
  in that window is caught.

Verified beyond the unit suite: 256 tests, the phase-5 audit re-run with and
without tokens to prove the new property is inert to the taint engine, and four
live field gates covering orphan removal, the reference gate, and the token's
three cases.

Two lessons this merge is worth remembering for:

- The live field test found what unit tests structurally could not — including a
  drop-order bug that made a completely successful repair exit 134, which is phase
  0b's invariant one layer down.
- Every fix round in this branch contained a defect the next review caught. The
  design held; the execution shell kept slipping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:41:33 -04:00
3 changed files with 201 additions and 0 deletions
+6
View File
@@ -1 +1,7 @@
/target
# 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/
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
}
+147
View File
@@ -0,0 +1,147 @@
{
description = "PixelPass P2P screen sharing CLI over iroh";
inputs = {
# Same channel the hosts run (nixos-config tracks nixos-26.05). The capture
# path talks to the live PipeWire daemon and the system PulseAudio server,
# so the client libraries here should come from the same release the server
# did.
nixpkgs.url = "github:nixos/nixpkgs/nixos-26.05";
# The Rust toolchain is pinned SEPARATELY from the system libraries, so a
# nixpkgs bump cannot move the compiler under the lint gate. nixpkgs 26.05
# ships 1.95.0; this crate was developed and verified on 1.97.1, and
# peerspeak — the sibling project this one is built against — has a clippy
# lint that differs between exactly those two versions. Keeping both repos
# on one pinned compiler means a check that passes here passes there.
#
# This is the reproducible alternative to rustup: the same exact-version
# control, but recorded in flake.lock, so a fresh clone resolves the
# identical toolchain rather than whatever rustup fetches 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) and what peerspeak pins.
# `default` is the rustup "default" profile — rustc, cargo, rust-std,
# rustfmt and clippy — so those are NOT listed separately below. No
# windows-gnu target here: pixelpass is Linux-only (portal/PipeWire/X11
# capture), unlike peerspeak which has a Windows port.
rustToolchain = pkgs.rust-bin.stable."1.97.1".default;
# GStreamer is driven as a SUBPROCESS (gst-launch-1.0 / gst-inspect-1.0),
# not linked — there is no gstreamer-sys in Cargo.lock. So these are PATH
# dependencies at runtime rather than build inputs, and `deps.rs` refuses
# to start a share if any are missing.
gstPlugins = with pkgs; [
gst_all_1.gstreamer # gst-launch-1.0 / gst-inspect-1.0
gst_all_1.gst-plugins-base # videoscale (quality-preset downscale)
gst_all_1.gst-plugins-good # pulsesrc, ximagesrc
gst_all_1.gst-plugins-bad # h264parse, mpegtsmux, aacparse, vah264enc
gst_all_1.gst-plugins-ugly # x264enc (software-encode fallback)
gst_all_1.gst-libav # avenc_aac
pipewire # pipewiresrc (Wayland capture; ships in this pkg)
];
# Opened with dlopen by the optional `--gui` front end (eframe/egui_glow/
# winit/glutin), never linked. Harmless for the default headless build.
guiRuntimeLibs = with pkgs; [
libGL
libxkbcommon
wayland
libx11
libxcursor
libxrandr
libxi
];
in
{
devShells.${system}.default = pkgs.mkShell {
nativeBuildInputs =
[ rustToolchain ]
++ (with pkgs; [
# Debian packaging (`cargo deb --no-build`). Build the binary
# inside a Debian/Ubuntu distrobox first so it links that distro's
# glibc — see the packaging notes in Cargo.toml.
cargo-deb
pkg-config
# pipewire-sys, libspa-sys and libpulse-sys all generate bindings
# with bindgen, which needs a real libclang at build time.
clang
])
++ gstPlugins
++ [
# The rest of what `deps::check_host_binaries` looks for.
pkgs.pulseaudio # `pactl` (PipeWire stays the actual audio server)
pkgs.mpv # the viewer-side player
pkgs.xwininfo # the `--window` click-picker on X11
# `--doctor` shells out to vainfo to confirm the VA-API H.264
# ENCODE entrypoint really exists. Without it the report can only
# say "vah264enc and a render node are present" and has to leave
# hardware encode unconfirmed — which matters, because a GPU
# missing that entrypoint produces no video at all under the
# default encoder rather than failing loudly.
pkgs.libva-utils
];
buildInputs =
with pkgs;
[
pipewire # pipewire-sys + libspa-sys
libpulseaudio # libpulse-sys: --repair reads/unloads Pulse modules
# x11rb is declared `default-features = false` here, which by itself
# is pure Rust — but Cargo unifies features across the graph, and
# arboard pulls x11rb with its `libxcb` feature on. That drags in
# as-raw-xcb-connection and makes the final link need -lxcb. It is a
# real link-time dependency of the binary, not an optional extra.
libxcb
]
++ guiRuntimeLibs;
# bindgen finds libclang through this variable specifically — having
# clang on PATH is not sufficient.
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
# NixOS keeps every GStreamer plugin in its own store path, so
# gst-launch-1.0 discovers them ONLY through this search path. Without
# it, pixelpass's `gst-inspect-1.0 --exists pipewiresrc` preflight fails
# even though the plugins are installed. Same reasoning as the
# GST_PLUGIN_SYSTEM_PATH_1_0 block in nixos-config hosts/darp5.
GST_PLUGIN_SYSTEM_PATH_1_0 = pkgs.lib.makeSearchPathOutput "lib" "lib/gstreamer-1.0" gstPlugins;
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath guiRuntimeLibs;
# 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 (and corrupt it outright for
# anything whose stdout is parsed, such as pixelpass's `--output json`).
shellHook = ''
if [ -t 1 ]; then
echo "pixelpass rustc $(rustc --version | cut -d' ' -f2) / cargo $(cargo --version | cut -d' ' -f2)"
echo " cargo build --release headless build (what peerspeak spawns)"
echo " cargo build --release --features gui with the egui front end"
echo " cargo test unit + integration tests"
echo " ./target/debug/pixelpass --doctor verify this machine can host"
echo
echo "GStreamer, pactl, mpv and xwininfo are on PATH in this shell, so"
echo "capture works here without a system rebuild."
fi
'';
};
};
}