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) <noreply@anthropic.com>
This commit is contained in:
parent
87b98193d8
commit
621823d930
|
|
@ -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",
|
||||
|
|
|
|||
13
README.md
13
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
27
devenv.nix
27
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
41
flake.nix
41
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue