From 621823d930b4be51ad65590d18eb4b402301bc27 Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 28 Jul 2026 00:23:29 +0200 Subject: [PATCH] nix, opus: let the packager choose where libopus comes from `opus` conflated two axes: decoding Ogg-Opus, and vendoring the C library to do it. So the only way to avoid a cmake build of libopus was to give up Opus playback -- the wrong trade for Nix, which already ships one. The adapter crate draws the line already: symphonia-adapter-libopus own `bundled` feature is what pulls opusic-sys/bundled and with it cmake. So declare the dependency `default-features = false` and add `opus-bundled = ["symphonia-adapter-libopus?/bundled"]`, forwarded up through crabidy-server and cbd. It stays in every `default`, so a plain cargo build still needs nothing installed; opting out is the packager act. The weak `?/` is load-bearing -- a plain `/bundled` would enable the optional dependency itself, and `opus-bundled` would quietly become a second "do we decode Opus" flag. The flake native build then drops cmake and takes libopus from nixpkgs; `headlessFeatures` already omitted opus-bundled, so it opts out for free. The cross build keeps the vendored copy: it links statically, and an unbundled -lopus would need a static aarch64 libopus staged for the target the way alsa-lib is. That exposed an older bug. rustc stamps no RUNPATH, and the -L from buildInputs arrives through NIX_LDFLAGS, which ld-wrapper does not mirror into the binary -- so the package linked cleanly and then refused to start. RUNPATH was empty, meaning libasound.so.2 had never resolved either: the package always depended on the caller having it on LD_LIBRARY_PATH, which this repo dev shell happens to set. autoPatchelfHook now fills the RUNPATH from buildInputs (plus stdenv.cc.cc for libgcc_s, the compiler own unwinding runtime, which no crate declares) and fails the build on anything it cannot resolve. Verified by running each binary under `env -i`, and with LD_BIND_NOW=1 so every opus symbol binds eagerly. Also: devenv sets OPUS_LIB_DIR, without which the unbundled build dies as "mold: fatal: library not found: opus"; check-features *builds* the two libopus variants rather than clippy-ing them, since clippy links nothing and cannot tell a resolvable -lopus from a missing one; and cbd was missing an `rss` pass-through, so the bundle could not select that provider alone. Requested alongside this: the spectrum shadows fall over 10 seconds instead of 4. The config default and SpectrumStyle::default are two spellings of one thing, so the config test now asserts the whole resolved style equals SpectrumStyle::default() rather than field-by-field. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.toml | 6 ++- README.md | 13 +++++-- architecture/build-features.md | 50 ++++++++++++++++++++++++ audio-player/Cargo.toml | 13 +++++-- cbd-tui/src/app/now_playing.rs | 4 +- cbd-tui/src/config.rs | 9 ++++- cbd/Cargo.toml | 11 +++++- crabidy-server/Cargo.toml | 10 +++-- devenv.nix | 27 ++++++++++++- docs/src/build-features.md | 70 +++++++++++++++++++++++----------- docs/src/clients/tui.md | 2 +- docs/src/config.md | 2 +- flake.nix | 41 +++++++++++++++----- plan/summary.md | 58 ++++++++++++++++++++++++++++ 14 files changed, 266 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3115219..7ea378d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,9 +73,11 @@ serde_json = "1" serde_urlencoded = "0.7" # Opus decoding: symphonia has no Opus decoder, so we demux Ogg-Opus with # symphonia's Ogg reader and decode via the libopus adapter (matches -# rodio's symphonia 0.5). The adapter bundles libopus (needs cmake). +# rodio's symphonia 0.5). default-features = false drops the adapter's own +# `bundled`, so audio-player's `opus-bundled` decides whether libopus is +# compiled from source (cmake) or linked from the system. symphonia = { version = "0.5", default-features = false, features = ["ogg"] } -symphonia-adapter-libopus = "0.2" +symphonia-adapter-libopus = { version = "0.2", default-features = false } stream-download = { version = "0.24", default-features = false, features = [ "reqwest", "reqwest-rustls", diff --git a/README.md b/README.md index 30e57bf..2e8319d 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,8 @@ another terminal, `cargo run -p cbd-tui`. Every provider — plus Opus decoding, the spectrum bars, the embedded web UI, and the TUI's desktop notifications — sits behind a Cargo feature, all on by default. Drop what you do not need and the dependencies go with -it: +it (`opus-bundled` is the one to drop if you would rather link the system +libopus than compile the vendored copy): ```sh # a local-files appliance: no network providers, no web UI, no FFT @@ -146,7 +147,7 @@ spectrum_gradient = true # shade them by height spectrum_top_color = "#b48ead" # what the shading reaches at the top spectrum_peak_color = "#81a1c1" # peak-hold shadows; "none" draws none spectrum_peak_fill = true # false: a thin rule at the peak instead -spectrum_peak_fall = 4.0 # seconds for a full-scale shadow to fall +spectrum_peak_fall = 10.0 # seconds for a full-scale shadow to fall spectrum_bar_width = 1 # least bar width, in cells spectrum_bar_gap = 1 # seam dividing two bars, in cells spectrum_row_gap = 1 # line dividing two rows, in eighths @@ -439,7 +440,13 @@ nix build .#crabidy-server-aarch64 scp ./result/bin/crabidy-server pi:/usr/local/bin/ ``` -Nix cross-compiles the Rust *and* the C dependencies (ALSA, aws-lc) +The native package links the libopus that nixpkgs ships (`opus` without +`opus-bundled`, see +[docs/src/build-features.md](docs/src/build-features.md)), so nothing is +compiled from vendored C and the binaries carry a RUNPATH — they run from a +bare login shell, not only from this repo's dev shell. + +Nix cross-compiles the Rust *and* the C dependencies (ALSA, libopus, aws-lc) hermetically on an x86_64 host — the isolated build avoids the host-linker pitfalls of cross-compiling in a plain shell. The aarch64 server **includes the embedded web UI**: the flake builds the `cbd-web` wasm bundle with trunk diff --git a/architecture/build-features.md b/architecture/build-features.md index 5b97320..8e84b8e 100644 --- a/architecture/build-features.md +++ b/architecture/build-features.md @@ -246,6 +246,56 @@ combinations, and `-D warnings` in the pre-commit hook. A curated matrix gives the coverage that matters; `cargo hack --each-feature` is available in devenv for a deeper sweep when the flags change. +**D12 — `opus-bundled` splits *decoding Opus* from *vendoring libopus* +(added 2026-07-28).** D6 treated the two as one axis, so the only way to +avoid the vendored C build was to give up Opus playback. That is the +wrong trade for a packaged build: Nix, and any distribution, would rather +link the libopus it already ships than run cmake inside the sandbox. + +The adapter crate already draws this line — `symphonia-adapter-libopus`'s +`bundled` feature is what pulls `opusic-sys/bundled` and with it the +`cmake` crate — so the split costs one feature and no code: + +```toml +# audio-player +default = ["opus", "opus-bundled"] +opus = ["dep:symphonia", "dep:symphonia-adapter-libopus"] +opus-bundled = ["symphonia-adapter-libopus?/bundled"] +``` + +with the workspace dependency declared `default-features = false` and +pass-throughs up through `crabidy-server` and `cbd`. Two details are +load-bearing: + +- The **weak** `?/` in `opus-bundled`. A plain `symphonia-adapter-libopus/bundled` + would enable the optional dependency itself, so `opus-bundled` alone + would silently switch Opus decoding back on — turning a "where does the + library come from" flag into a second "do we decode Opus" flag. +- `opus-bundled` stays in `default`, so a plain `cargo build` still needs + nothing installed. Opting *out* is the packager's explicit act, not the + developer's default. + +Consequences for the Nix builds (D10): the native package drops `cmake`, +takes `libopus` as a `buildInput`, and leaves `opus-bundled` out of its +feature list. The aarch64 cross build keeps the vendored copy — it links +statically, and an unbundled `-lopus` would need a static aarch64 libopus +staged for the target the way `alsa-lib` is. `devenv.nix` sets +`OPUS_LIB_DIR` so the unbundled path links in the dev shell too, where the +linker knows nothing of the store. + +**D13 — the native Nix package sets its own RUNPATH (added +2026-07-28).** Uncovered by D12: rustc stamps no RUNPATH, and the `-L` +that `buildInputs` contributes reaches the linker through `NIX_LDFLAGS`, +which ld-wrapper does not mirror into the binary. The installed binaries +therefore linked cleanly and then refused to *start* wherever the +libraries were not already on `LD_LIBRARY_PATH` — a latent bug for +`libasound` since the package was first written, which the added +`libopus` only made louder. `autoPatchelfHook` on the final derivation +fills the RUNPATH from `buildInputs` (plus `stdenv.cc.cc`'s `libgcc_s`, +which is the compiler's own runtime and so declared by no crate) and +fails the build on anything it cannot resolve. Verified by running each +binary under `env -i`. + ## Structure ```d2 diff --git a/audio-player/Cargo.toml b/audio-player/Cargo.toml index c01edff..b6159f1 100644 --- a/audio-player/Cargo.toml +++ b/audio-player/Cargo.toml @@ -4,14 +4,21 @@ version.workspace = true edition.workspace = true # The one axis worth a flag: Ogg-Opus decoding pulls symphonia *and* -# `symphonia-adapter-libopus`, which bundles libopus and so makes cmake + -# ninja build requirements. Everything else in this crate (HLS, the +# `symphonia-adapter-libopus`. Everything else in this crate (HLS, the # windowed-HTTP source, the spectrum tap) shares its dependencies with # code that always ships, so gating it would buy nothing # (architecture/build-features.md D6/D7/D8). [features] -default = ["opus"] +default = ["opus", "opus-bundled"] opus = ["dep:symphonia", "dep:symphonia-adapter-libopus"] +# Where libopus comes from (architecture/build-features.md D12). On (the +# default) compiles it from the vendored source, which needs cmake and a C +# compiler but nothing installed. Off links the system libopus instead, so a +# packaged build can use the distribution's copy: `--no-default-features +# --features opus`. The weak `?/` is load-bearing — it must not pull the +# optional adapter in by itself, or `opus-bundled` would silently re-enable +# `opus`. +opus-bundled = ["symphonia-adapter-libopus?/bundled"] [dependencies] anyhow.workspace = true diff --git a/cbd-tui/src/app/now_playing.rs b/cbd-tui/src/app/now_playing.rs index a8c436f..c53920f 100644 --- a/cbd-tui/src/app/now_playing.rs +++ b/cbd-tui/src/app/now_playing.rs @@ -77,7 +77,7 @@ impl Default for SpectrumStyle { gradient: true, peak: Some(COLOR_PRIMARY), peak_fill: true, - peak_fall: 4.0, + peak_fall: 10.0, bar_width: 1, bar_gap: 1, row_gap: 1, @@ -816,7 +816,7 @@ mod tests { (1, 1, 1), "connected bars, a seam between them, a line between rows" ); - assert_eq!(style.peak_fall, 4.0); + assert_eq!(style.peak_fall, 10.0, "shadows linger, they do not chase"); } #[test] diff --git a/cbd-tui/src/config.rs b/cbd-tui/src/config.rs index 51c05be..e7a027a 100644 --- a/cbd-tui/src/config.rs +++ b/cbd-tui/src/config.rs @@ -215,7 +215,7 @@ pub struct ServerConfig { /// Seconds a full-scale shadow takes to fall to the floor, 0.05-60 /// (out of range is clamped with a warning). Higher lingers longer. - #[default(4.0)] + #[default(10.0)] #[clap(long)] pub spectrum_peak_fall: f32, @@ -468,7 +468,12 @@ mod tests { assert_eq!(style.bar_gap, 1, "a one-cell seam"); assert_eq!(style.row_gap, 1, "an eighth of a row between segments"); assert!(style.peak_fill, "shadows filled"); - assert_eq!(style.peak_fall, 4.0); + assert_eq!(style.peak_fall, 10.0, "ten seconds to the floor"); + assert_eq!( + style, + crate::app::SpectrumStyle::default(), + "the config defaults and the renderer's own must agree" + ); } #[test] diff --git a/cbd/Cargo.toml b/cbd/Cargo.toml index 7a6fe4f..5765188 100644 --- a/cbd/Cargo.toml +++ b/cbd/Cargo.toml @@ -7,7 +7,14 @@ edition.workspace = true # (architecture/build-features.md D1). `cargo build -p cbd # --no-default-features --features fs,opus` is a local-files-only bundle. [features] -default = ["all-providers", "opus", "spectrum", "web-ui", "notifications"] +default = [ + "all-providers", + "opus", + "opus-bundled", + "spectrum", + "web-ui", + "notifications", +] all-providers = ["crabidy-server/all-providers"] tidal = ["crabidy-server/tidal"] @@ -16,8 +23,10 @@ fyyd = ["crabidy-server/fyyd"] abs = ["crabidy-server/abs"] soundcloud = ["crabidy-server/soundcloud"] jamendo = ["crabidy-server/jamendo"] +rss = ["crabidy-server/rss"] fs = ["crabidy-server/fs"] opus = ["crabidy-server/opus"] +opus-bundled = ["crabidy-server/opus-bundled"] spectrum = ["crabidy-server/spectrum"] web-ui = ["crabidy-server/web-ui"] notifications = ["cbd-tui/notifications"] diff --git a/crabidy-server/Cargo.toml b/crabidy-server/Cargo.toml index 92eca57..872f9b6 100644 --- a/crabidy-server/Cargo.toml +++ b/crabidy-server/Cargo.toml @@ -13,7 +13,7 @@ path = "src/main.rs" # names in `crabidy-server.toml`'s `providers` list (D3), so one # vocabulary covers the compile-time and the runtime switch. [features] -default = ["all-providers", "opus", "spectrum", "web-ui"] +default = ["all-providers", "opus", "opus-bundled", "spectrum", "web-ui"] # Every provider this binary can mount. all-providers = [ @@ -44,9 +44,13 @@ rss = ["dep:rssdy", "_any-provider"] # persistence, and the `scan` command. Off means the server keeps its # queue in memory only. fs = ["dep:fsdy", "dep:blake3", "dep:reqwest", "_any-provider"] -# Ogg-Opus decoding. Off also drops the bundled libopus C build -# (cmake + ninja) and `.opus` from what `scan` indexes (D6). +# Ogg-Opus decoding. Off also drops the libopus dependency entirely and +# `.opus` from what `scan` indexes (D6). opus = ["audio-player/opus"] +# Compile the vendored libopus (needs cmake) instead of linking the system +# one. On by default so a plain `cargo build` needs nothing installed; a +# distribution build drops it and links libopus from the system (D12). +opus-bundled = ["audio-player/opus-bundled"] # The server-side FFT that feeds clients' spectrum bars (D8). spectrum = ["dep:realfft"] # The embedded web client (architecture/web-client.md). Disable for a diff --git a/devenv.nix b/devenv.nix index 7fb202a..d5558a0 100644 --- a/devenv.nix +++ b/devenv.nix @@ -8,7 +8,13 @@ let pkgs-unstable = import inputs.nixpkgs-unstable { system = pkgs.stdenv.system; }; - commonLibs = with pkgs; [ alsa-lib ]; + # Linked by every build that plays audio. libopus is only reached by an + # unbundled `opus` build (see OPUS_LIB_DIR below); the default `opus-bundled` + # compiles its own copy and ignores it. + commonLibs = with pkgs; [ + alsa-lib + libopus + ]; extraPackages = with pkgs; [ d2 @@ -46,6 +52,12 @@ in env = { LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath commonLibs; + # Where an unbundled Opus build finds libopus. opusic-sys emits a bare + # `-lopus` and leaves the search path to the linker, which in a nix shell + # knows nothing of the store — so `--no-default-features --features + # …,opus` would fail with "mold: library not found: opus". This is the + # crate's own knob and the bundled build ignores it. + OPUS_LIB_DIR = "${pkgs.libopus}/lib"; }; # https://devenv.sh/packages/ @@ -99,6 +111,12 @@ in echo "==> test $*" cargo test "$@" } + # Clippy never links a binary, so it cannot tell a resolvable `-lopus` + # from a missing one. The unbundled Opus build is checked by building it. + build_it() { + echo "==> build $*" + cargo build "$@" + } # The two extremes, tests included. clippy -p crabidy-server @@ -119,8 +137,15 @@ in clippy -p crabidy-server --no-default-features --features fs,opus clippy -p crabidy-server --no-default-features --features tidal,web-ui,opus,spectrum + # Where libopus comes from. `opus` alone means the system copy (what the + # nix package and a distribution build use); adding `opus-bundled` + # compiles the vendored source. Both must link, so both are built. + build_it -p crabidy-server --no-default-features --features fs,opus + build_it -p crabidy-server --no-default-features --features fs,opus,opus-bundled + # The other feature-carrying crates. clippy -p audio-player --no-default-features + clippy -p audio-player --no-default-features --features opus clippy -p cbd-tui --no-default-features test_it -p cbd-tui --no-default-features clippy -p cbd --no-default-features diff --git a/docs/src/build-features.md b/docs/src/build-features.md index 18411fc..a5c9e67 100644 --- a/docs/src/build-features.md +++ b/docs/src/build-features.md @@ -28,19 +28,20 @@ The same list goes into the server's startup log, so a support question ## The features -| Feature | Turning it off drops | -| ------------ | ----------------------------------------------------------- | -| `tidal` | the `/tidal` provider (`tidaldy`) | -| `youtube` | the `/youtube` provider (`ytdy`, and with it `rustypipe`) | -| `fyyd` | the `/fyyd` podcast provider | -| `abs` | the `/abs` audiobookshelf provider | -| `soundcloud` | the `/soundcloud` provider | -| `jamendo` | the `/jamendo` provider | -| `rss` | the `/rss` podcast-subscription provider | -| `fs` | local files **and persistent state** — see below | -| `opus` | Ogg-Opus decoding (`symphonia` + a bundled libopus C build) | -| `spectrum` | the server-side FFT feeding clients' spectrum bars | -| `web-ui` | the embedded web client (`tonic-web` + the wasm bundle) | +| Feature | Turning it off drops | +| -------------- | --------------------------------------------------------- | +| `tidal` | the `/tidal` provider (`tidaldy`) | +| `youtube` | the `/youtube` provider (`ytdy`, and with it `rustypipe`) | +| `fyyd` | the `/fyyd` podcast provider | +| `abs` | the `/abs` audiobookshelf provider | +| `soundcloud` | the `/soundcloud` provider | +| `jamendo` | the `/jamendo` provider | +| `rss` | the `/rss` podcast-subscription provider | +| `fs` | local files **and persistent state** — see below | +| `opus` | Ogg-Opus decoding (`symphonia` + libopus) | +| `opus-bundled` | compiling libopus; links the system copy instead | +| `spectrum` | the server-side FFT feeding clients' spectrum bars | +| `web-ui` | the embedded web client (`tonic-web` + the wasm bundle) | Plus two conveniences: `all-providers` enables the eight provider features at once, and `cbd` (the bundle) mirrors every feature above and adds @@ -67,16 +68,39 @@ clients report an ordinary error instead of hanging. If you want `/fs` but not `/crabidy`, keep the feature and prune the `providers` list at runtime instead — that is what it is for. -### `opus` and the libopus build +### `opus` and where libopus comes from `symphonia` (and so rodio) has no Opus decoder, so Ogg-Opus files are -decoded through a bundled libopus, which needs `cmake` at build time. -Turning `opus` off removes both the crates and that build requirement. +decoded through libopus. Turning `opus` off removes the crates, the library, +and the C build with them. -The feature also decides whether `scan` treats `.opus` files as playable at -all, so a build that cannot decode Opus will not index Opus files either. An -Opus file that reaches such a build fails to decode with a message naming -the missing feature and is skipped, exactly like any unplayable file. +`opus-bundled` picks *which* libopus. It is on by default, so a plain +`cargo build` compiles the vendored source and needs `cmake` and a C +compiler but nothing installed. Drop it — `--no-default-features --features +…,opus` — and the build links the system libopus instead: + +```sh +# a distribution build: Opus decoding, no vendored C build +cargo build --release -p crabidy-server \ + --no-default-features --features all-providers,opus,spectrum,web-ui +``` + +That is what this repo's Nix package does, taking libopus from nixpkgs. The +build script emits a bare `-lopus` and leaves the search path to the linker +(the bindings are pregenerated, so no headers are needed); point +`OPUS_LIB_DIR` at the directory holding the library if the linker does not +find it on its own, as the dev shell does. The aarch64 cross build keeps +`opus-bundled`, because a static binary needs a static libopus for the +target and compiling the vendored copy is the way to get one. + +`opus-bundled` without `opus` does nothing at all — it only chooses a source +for a library the `opus` feature decides to use. + +The `opus` feature also decides whether `scan` treats `.opus` files as +playable at all, so a build that cannot decode Opus will not index Opus +files either. An Opus file that reaches such a build fails to decode with a +message naming the missing feature and is skipped, exactly like any +unplayable file. ### `spectrum` and `web-ui` @@ -132,5 +156,7 @@ the feature matrix checks, not a useful deployment. `devenv shell -- check-features` runs the curated matrix — defaults, no features, each provider alone, each extra dropped, both examples above, and -the client crates — and requires every one to be clippy-clean. Run it after -changing anything that sits behind a feature. +the client crates — and requires every one to be clippy-clean. The two +libopus variants are *built*, not just checked: clippy links nothing, so +only a real build can tell a resolvable `-lopus` from a missing one. Run it +after changing anything that sits behind a feature. diff --git a/docs/src/clients/tui.md b/docs/src/clients/tui.md index e706d60..2390e74 100644 --- a/docs/src/clients/tui.md +++ b/docs/src/clients/tui.md @@ -200,7 +200,7 @@ primary blue by default, so it reads as a shadow of the red bar rather than part of it. It shows what a transient reached after the bar has dropped away. - `spectrum_peak_fall` is how long, in seconds, a full-scale shadow takes to - fall to the floor (4 by default). It is real time, not frames, so the + fall to the floor (10 by default). It is real time, not frames, so the server's frame rate does not change the feel. - `spectrum_peak_fill = false` draws a thin rule (`▔`) at the peak instead of filling the space below it — also the thing to do if your terminal font diff --git a/docs/src/config.md b/docs/src/config.md index 068774b..f700bb1 100644 --- a/docs/src/config.md +++ b/docs/src/config.md @@ -130,7 +130,7 @@ spectrum_peak_color = "#81a1c1" spectrum_peak_fill = true # Seconds a full-scale shadow takes to fall to the floor, 0.05-60. -spectrum_peak_fall = 4.0 +spectrum_peak_fall = 10.0 # Least bar width in cells, 1-16. Bars take any spare columns beyond # this, so raise it only to force wider bars and fewer bands. diff --git a/flake.nix b/flake.nix index 2daa205..eb5083c 100644 --- a/flake.nix +++ b/flake.nix @@ -63,7 +63,9 @@ # or this package ships an empty server. The # aarch64 server (below) takes the full defaults and stages the wasm # bundle. `notifications` is cbd-tui's own default and needs no - # mention here. + # mention here. `opus-bundled` is deliberately absent: Opus decoding + # is on, but libopus comes from nixpkgs rather than a cmake build of + # the vendored source. headlessFeatures = "all-providers,opus,spectrum,notifications"; # --- Web UI (wasm) bundle --------------------------------------------- @@ -140,16 +142,18 @@ cargoExtraArgs = "--locked --no-default-features --features ${headlessFeatures} ${workspaceBins}"; # protoc + pkg-config are build-host tools (run during the build); # with strictDeps they must be nativeBuildInputs or alsa-sys cannot - # find pkg-config. alsa-lib is the actual linked library. cmake - # builds the bundled libopus (opusic-sys, via the `opus` feature) — - # deliberately without ninja, whose mere presence flips cmake's - # generator and then clashes with a cached build dir. + # find pkg-config. alsa-lib and libopus are the actual linked + # libraries: `headlessFeatures` leaves `opus-bundled` off, so + # opusic-sys emits a plain `-lopus` instead of compiling the + # vendored copy with cmake, and nixpkgs' libopus satisfies it. nativeBuildInputs = [ pkgs.protobuf pkgs.pkg-config - pkgs.cmake ]; - buildInputs = [ pkgs.alsa-lib ]; + buildInputs = [ + pkgs.alsa-lib + pkgs.libopus + ]; # audio-player links libasound; yt-dlp for /youtube stays a runtime # dependency, not wired here. }; @@ -159,6 +163,20 @@ // { cargoArtifacts = nativeDeps; doCheck = false; # tests need a config/network; `nix flake check` can run them later + # rustc stamps no RUNPATH of its own, and the `-L` that buildInputs + # contributes arrives through NIX_LDFLAGS, which ld-wrapper does not + # mirror into the binary. Without this the binaries link cleanly and + # then refuse to *start* — "libasound.so.2: cannot open shared + # object file" — anywhere the libraries are not already on + # LD_LIBRARY_PATH, which is to say everywhere outside this repo's + # dev shell. autoPatchelfHook walks the finished binaries and fills + # their RUNPATH from buildInputs, so an installed crabidy runs on a + # bare login shell. It also fails the build on any library it cannot + # resolve, which is how a missing buildInput should surface. + nativeBuildInputs = nativeArgs.nativeBuildInputs ++ [ pkgs.autoPatchelfHook ]; + # Rust links libgcc_s for unwinding; it is the compiler's own + # runtime, so no crate declares it and the hook cannot guess it. + buildInputs = nativeArgs.buildInputs ++ [ (pkgs.lib.getLib pkgs.stdenv.cc.cc) ]; } ); @@ -193,8 +211,13 @@ ''; # Build scripts / proc-macros compile for the build host; deps for the - # target use the cross toolchain. protoc, pkg-config, and cmake (for - # the bundled libopus) are host tools. + # target use the cross toolchain. protoc, pkg-config, and cmake are + # host tools. Unlike the native build this one keeps the default + # `opus-bundled`: cmake cross-compiles the vendored libopus into the + # static binary, whereas an unbundled `-lopus` would need a static + # aarch64 libopus staged for the target the way alsa-lib is. cmake + # comes deliberately without ninja, whose mere presence flips cmake's + # generator and then clashes with a cached build dir. nativeBuildInputs = [ pkgs.protobuf pkgs.pkg-config diff --git a/plan/summary.md b/plan/summary.md index 1e5a0e0..3e5ed73 100644 --- a/plan/summary.md +++ b/plan/summary.md @@ -1812,3 +1812,61 @@ the service 25 methods and the suite stayed green. It now reads the method names out of `crabidy.proto` with `include_str!` and compares sets, so the next RPC cannot be forgotten. A count copied out of a file is not a check against that file. + +## Unbundling libopus, and the RUNPATH it exposed (2026-07-28) + +From a report of installing the flake: the native Nix build needed `cmake` +only to compile the vendored libopus, while nixpkgs already ships one. The +axes were conflated — `opus` meant *decode Opus* **and** *vendor the C +library* — so the only way to skip the C build was to give up Opus playback. + +Splitting them costs one feature and no code, because the adapter crate +already draws the line: `symphonia-adapter-libopus`'s own `bundled` feature is +what pulls `opusic-sys/bundled` and with it the `cmake` crate. So the +workspace dependency is declared `default-features = false`, `audio-player` +grows `opus-bundled = ["symphonia-adapter-libopus?/bundled"]`, and +`crabidy-server` and `cbd` forward it. It stays in every `default`, so a plain +`cargo build` still needs nothing installed; opting out is the packager's +explicit act. The **weak** `?/` is load-bearing: a plain +`symphonia-adapter-libopus/bundled` would enable the optional dependency +itself, so `opus-bundled` alone would quietly switch Opus decoding back on and +the flag would stop being about *where the library comes from*. Checked with +`cargo tree`, not by reading: `--features opus-bundled` alone pulls neither +the adapter nor `opusic-sys`. + +The flake's native build then drops `cmake` and takes `libopus` as a +`buildInput`; `headlessFeatures` already omitted `opus-bundled`, so it opts +out for free. The cross build keeps the vendored copy on purpose — it links +statically, and an unbundled `-lopus` would need a static aarch64 libopus +staged for the target the way `alsa-lib` is. + +**The build then succeeded and produced a binary that could not start.** +`ldd` said `libopus.so.0 => not found`: rustc stamps no RUNPATH, and the `-L` +that `buildInputs` contributes arrives through `NIX_LDFLAGS`, which +ld-wrapper does not mirror into the binary. Chasing it turned up something +older — `RUNPATH: []` — so `libasound.so.2` had never been resolvable either; +the package had always depended on the caller having it on +`LD_LIBRARY_PATH`, which this repo's dev shell happens to set. The added +`libopus` only made a latent bug audible. `autoPatchelfHook` on the final +derivation fills the RUNPATH from `buildInputs` and, just as usefully, fails +the build on anything it cannot resolve — that is how it named `libgcc_s.so.1`, +the compiler's own unwinding runtime, which no crate declares and which +therefore needs `stdenv.cc.cc` added explicitly. Verified the way the bug +showed up in the first place: every binary run under `env -i`. + +Two verification notes worth keeping. `check-features` gained real *builds* +for the two libopus variants, because clippy links nothing and so cannot tell +a resolvable `-lopus` from a missing one — the dev shell's link failure +("mold: fatal: library not found: opus") is invisible to the matrix +otherwise, and is why `devenv.nix` now sets `OPUS_LIB_DIR`. And `cbd` was +missing an `rss` pass-through, so the bundle could not select that provider +alone even though `all-providers` reached it; found while adding the +`opus-bundled` pass-through beside it. + +## Spectrum: a slower default fall (2026-07-28) + +`spectrum_peak_fall` moves from 4 s to 10 s. Everything else in the requested +default block was already the default. The renderer's `SpectrumStyle::default` +and the config's `#[default]` are two spellings of one thing, so the config +test now asserts the whole resolved style equals `SpectrumStyle::default()` +rather than field-by-field — the pair cannot drift apart silently.