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"
|
serde_urlencoded = "0.7"
|
||||||
# Opus decoding: symphonia has no Opus decoder, so we demux Ogg-Opus with
|
# Opus decoding: symphonia has no Opus decoder, so we demux Ogg-Opus with
|
||||||
# symphonia's Ogg reader and decode via the libopus adapter (matches
|
# 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 = { 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 = [
|
stream-download = { version = "0.24", default-features = false, features = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-rustls",
|
"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
|
Every provider — plus Opus decoding, the spectrum bars, the embedded web
|
||||||
UI, and the TUI's desktop notifications — sits behind a Cargo feature,
|
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
|
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
|
```sh
|
||||||
# a local-files appliance: no network providers, no web UI, no FFT
|
# 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_top_color = "#b48ead" # what the shading reaches at the top
|
||||||
spectrum_peak_color = "#81a1c1" # peak-hold shadows; "none" draws none
|
spectrum_peak_color = "#81a1c1" # peak-hold shadows; "none" draws none
|
||||||
spectrum_peak_fill = true # false: a thin rule at the peak instead
|
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_width = 1 # least bar width, in cells
|
||||||
spectrum_bar_gap = 1 # seam dividing two bars, in cells
|
spectrum_bar_gap = 1 # seam dividing two bars, in cells
|
||||||
spectrum_row_gap = 1 # line dividing two rows, in eighths
|
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/
|
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
|
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
|
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
|
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
|
gives the coverage that matters; `cargo hack --each-feature` is
|
||||||
available in devenv for a deeper sweep when the flags change.
|
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
|
## Structure
|
||||||
|
|
||||||
```d2
|
```d2
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,21 @@ version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
# The one axis worth a flag: Ogg-Opus decoding pulls symphonia *and*
|
# The one axis worth a flag: Ogg-Opus decoding pulls symphonia *and*
|
||||||
# `symphonia-adapter-libopus`, which bundles libopus and so makes cmake +
|
# `symphonia-adapter-libopus`. Everything else in this crate (HLS, the
|
||||||
# ninja build requirements. Everything else in this crate (HLS, the
|
|
||||||
# windowed-HTTP source, the spectrum tap) shares its dependencies with
|
# windowed-HTTP source, the spectrum tap) shares its dependencies with
|
||||||
# code that always ships, so gating it would buy nothing
|
# code that always ships, so gating it would buy nothing
|
||||||
# (architecture/build-features.md D6/D7/D8).
|
# (architecture/build-features.md D6/D7/D8).
|
||||||
[features]
|
[features]
|
||||||
default = ["opus"]
|
default = ["opus", "opus-bundled"]
|
||||||
opus = ["dep:symphonia", "dep:symphonia-adapter-libopus"]
|
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]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ impl Default for SpectrumStyle {
|
||||||
gradient: true,
|
gradient: true,
|
||||||
peak: Some(COLOR_PRIMARY),
|
peak: Some(COLOR_PRIMARY),
|
||||||
peak_fill: true,
|
peak_fill: true,
|
||||||
peak_fall: 4.0,
|
peak_fall: 10.0,
|
||||||
bar_width: 1,
|
bar_width: 1,
|
||||||
bar_gap: 1,
|
bar_gap: 1,
|
||||||
row_gap: 1,
|
row_gap: 1,
|
||||||
|
|
@ -816,7 +816,7 @@ mod tests {
|
||||||
(1, 1, 1),
|
(1, 1, 1),
|
||||||
"connected bars, a seam between them, a line between rows"
|
"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]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ pub struct ServerConfig {
|
||||||
|
|
||||||
/// Seconds a full-scale shadow takes to fall to the floor, 0.05-60
|
/// 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.
|
/// (out of range is clamped with a warning). Higher lingers longer.
|
||||||
#[default(4.0)]
|
#[default(10.0)]
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub spectrum_peak_fall: f32,
|
pub spectrum_peak_fall: f32,
|
||||||
|
|
||||||
|
|
@ -468,7 +468,12 @@ mod tests {
|
||||||
assert_eq!(style.bar_gap, 1, "a one-cell seam");
|
assert_eq!(style.bar_gap, 1, "a one-cell seam");
|
||||||
assert_eq!(style.row_gap, 1, "an eighth of a row between segments");
|
assert_eq!(style.row_gap, 1, "an eighth of a row between segments");
|
||||||
assert!(style.peak_fill, "shadows filled");
|
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]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,14 @@ edition.workspace = true
|
||||||
# (architecture/build-features.md D1). `cargo build -p cbd
|
# (architecture/build-features.md D1). `cargo build -p cbd
|
||||||
# --no-default-features --features fs,opus` is a local-files-only bundle.
|
# --no-default-features --features fs,opus` is a local-files-only bundle.
|
||||||
[features]
|
[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"]
|
all-providers = ["crabidy-server/all-providers"]
|
||||||
tidal = ["crabidy-server/tidal"]
|
tidal = ["crabidy-server/tidal"]
|
||||||
|
|
@ -16,8 +23,10 @@ fyyd = ["crabidy-server/fyyd"]
|
||||||
abs = ["crabidy-server/abs"]
|
abs = ["crabidy-server/abs"]
|
||||||
soundcloud = ["crabidy-server/soundcloud"]
|
soundcloud = ["crabidy-server/soundcloud"]
|
||||||
jamendo = ["crabidy-server/jamendo"]
|
jamendo = ["crabidy-server/jamendo"]
|
||||||
|
rss = ["crabidy-server/rss"]
|
||||||
fs = ["crabidy-server/fs"]
|
fs = ["crabidy-server/fs"]
|
||||||
opus = ["crabidy-server/opus"]
|
opus = ["crabidy-server/opus"]
|
||||||
|
opus-bundled = ["crabidy-server/opus-bundled"]
|
||||||
spectrum = ["crabidy-server/spectrum"]
|
spectrum = ["crabidy-server/spectrum"]
|
||||||
web-ui = ["crabidy-server/web-ui"]
|
web-ui = ["crabidy-server/web-ui"]
|
||||||
notifications = ["cbd-tui/notifications"]
|
notifications = ["cbd-tui/notifications"]
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ path = "src/main.rs"
|
||||||
# names in `crabidy-server.toml`'s `providers` list (D3), so one
|
# names in `crabidy-server.toml`'s `providers` list (D3), so one
|
||||||
# vocabulary covers the compile-time and the runtime switch.
|
# vocabulary covers the compile-time and the runtime switch.
|
||||||
[features]
|
[features]
|
||||||
default = ["all-providers", "opus", "spectrum", "web-ui"]
|
default = ["all-providers", "opus", "opus-bundled", "spectrum", "web-ui"]
|
||||||
|
|
||||||
# Every provider this binary can mount.
|
# Every provider this binary can mount.
|
||||||
all-providers = [
|
all-providers = [
|
||||||
|
|
@ -44,9 +44,13 @@ rss = ["dep:rssdy", "_any-provider"]
|
||||||
# persistence, and the `scan` command. Off means the server keeps its
|
# persistence, and the `scan` command. Off means the server keeps its
|
||||||
# queue in memory only.
|
# queue in memory only.
|
||||||
fs = ["dep:fsdy", "dep:blake3", "dep:reqwest", "_any-provider"]
|
fs = ["dep:fsdy", "dep:blake3", "dep:reqwest", "_any-provider"]
|
||||||
# Ogg-Opus decoding. Off also drops the bundled libopus C build
|
# Ogg-Opus decoding. Off also drops the libopus dependency entirely and
|
||||||
# (cmake + ninja) and `.opus` from what `scan` indexes (D6).
|
# `.opus` from what `scan` indexes (D6).
|
||||||
opus = ["audio-player/opus"]
|
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).
|
# The server-side FFT that feeds clients' spectrum bars (D8).
|
||||||
spectrum = ["dep:realfft"]
|
spectrum = ["dep:realfft"]
|
||||||
# The embedded web client (architecture/web-client.md). Disable for a
|
# The embedded web client (architecture/web-client.md). Disable for a
|
||||||
|
|
|
||||||
27
devenv.nix
27
devenv.nix
|
|
@ -8,7 +8,13 @@
|
||||||
|
|
||||||
let
|
let
|
||||||
pkgs-unstable = import inputs.nixpkgs-unstable { system = pkgs.stdenv.system; };
|
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; [
|
extraPackages = with pkgs; [
|
||||||
d2
|
d2
|
||||||
|
|
@ -46,6 +52,12 @@ in
|
||||||
|
|
||||||
env = {
|
env = {
|
||||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath commonLibs;
|
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/
|
# https://devenv.sh/packages/
|
||||||
|
|
@ -99,6 +111,12 @@ in
|
||||||
echo "==> test $*"
|
echo "==> test $*"
|
||||||
cargo 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.
|
# The two extremes, tests included.
|
||||||
clippy -p crabidy-server
|
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 fs,opus
|
||||||
clippy -p crabidy-server --no-default-features --features tidal,web-ui,opus,spectrum
|
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.
|
# The other feature-carrying crates.
|
||||||
clippy -p audio-player --no-default-features
|
clippy -p audio-player --no-default-features
|
||||||
|
clippy -p audio-player --no-default-features --features opus
|
||||||
clippy -p cbd-tui --no-default-features
|
clippy -p cbd-tui --no-default-features
|
||||||
test_it -p cbd-tui --no-default-features
|
test_it -p cbd-tui --no-default-features
|
||||||
clippy -p cbd --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
|
## The features
|
||||||
|
|
||||||
| Feature | Turning it off drops |
|
| Feature | Turning it off drops |
|
||||||
| ------------ | ----------------------------------------------------------- |
|
| -------------- | --------------------------------------------------------- |
|
||||||
| `tidal` | the `/tidal` provider (`tidaldy`) |
|
| `tidal` | the `/tidal` provider (`tidaldy`) |
|
||||||
| `youtube` | the `/youtube` provider (`ytdy`, and with it `rustypipe`) |
|
| `youtube` | the `/youtube` provider (`ytdy`, and with it `rustypipe`) |
|
||||||
| `fyyd` | the `/fyyd` podcast provider |
|
| `fyyd` | the `/fyyd` podcast provider |
|
||||||
| `abs` | the `/abs` audiobookshelf provider |
|
| `abs` | the `/abs` audiobookshelf provider |
|
||||||
| `soundcloud` | the `/soundcloud` provider |
|
| `soundcloud` | the `/soundcloud` provider |
|
||||||
| `jamendo` | the `/jamendo` provider |
|
| `jamendo` | the `/jamendo` provider |
|
||||||
| `rss` | the `/rss` podcast-subscription provider |
|
| `rss` | the `/rss` podcast-subscription provider |
|
||||||
| `fs` | local files **and persistent state** — see below |
|
| `fs` | local files **and persistent state** — see below |
|
||||||
| `opus` | Ogg-Opus decoding (`symphonia` + a bundled libopus C build) |
|
| `opus` | Ogg-Opus decoding (`symphonia` + libopus) |
|
||||||
| `spectrum` | the server-side FFT feeding clients' spectrum bars |
|
| `opus-bundled` | compiling libopus; links the system copy instead |
|
||||||
| `web-ui` | the embedded web client (`tonic-web` + the wasm bundle) |
|
| `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
|
Plus two conveniences: `all-providers` enables the eight provider features
|
||||||
at once, and `cbd` (the bundle) mirrors every feature above and adds
|
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
|
If you want `/fs` but not `/crabidy`, keep the feature and prune the
|
||||||
`providers` list at runtime instead — that is what it is for.
|
`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
|
`symphonia` (and so rodio) has no Opus decoder, so Ogg-Opus files are
|
||||||
decoded through a bundled libopus, which needs `cmake` at build time.
|
decoded through libopus. Turning `opus` off removes the crates, the library,
|
||||||
Turning `opus` off removes both the crates and that build requirement.
|
and the C build with them.
|
||||||
|
|
||||||
The feature also decides whether `scan` treats `.opus` files as playable at
|
`opus-bundled` picks *which* libopus. It is on by default, so a plain
|
||||||
all, so a build that cannot decode Opus will not index Opus files either. An
|
`cargo build` compiles the vendored source and needs `cmake` and a C
|
||||||
Opus file that reaches such a build fails to decode with a message naming
|
compiler but nothing installed. Drop it — `--no-default-features --features
|
||||||
the missing feature and is skipped, exactly like any unplayable file.
|
…,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`
|
### `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
|
`devenv shell -- check-features` runs the curated matrix — defaults, no
|
||||||
features, each provider alone, each extra dropped, both examples above, and
|
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
|
the client crates — and requires every one to be clippy-clean. The two
|
||||||
changing anything that sits behind a feature.
|
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.
|
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
|
- `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.
|
server's frame rate does not change the feel.
|
||||||
- `spectrum_peak_fill = false` draws a thin rule (`▔`) at the peak instead of
|
- `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
|
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
|
spectrum_peak_fill = true
|
||||||
|
|
||||||
# Seconds a full-scale shadow takes to fall to the floor, 0.05-60.
|
# 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
|
# 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.
|
# 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
|
# or this package ships an empty server. The
|
||||||
# aarch64 server (below) takes the full defaults and stages the wasm
|
# aarch64 server (below) takes the full defaults and stages the wasm
|
||||||
# bundle. `notifications` is cbd-tui's own default and needs no
|
# 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";
|
headlessFeatures = "all-providers,opus,spectrum,notifications";
|
||||||
|
|
||||||
# --- Web UI (wasm) bundle ---------------------------------------------
|
# --- Web UI (wasm) bundle ---------------------------------------------
|
||||||
|
|
@ -140,16 +142,18 @@
|
||||||
cargoExtraArgs = "--locked --no-default-features --features ${headlessFeatures} ${workspaceBins}";
|
cargoExtraArgs = "--locked --no-default-features --features ${headlessFeatures} ${workspaceBins}";
|
||||||
# protoc + pkg-config are build-host tools (run during the build);
|
# protoc + pkg-config are build-host tools (run during the build);
|
||||||
# with strictDeps they must be nativeBuildInputs or alsa-sys cannot
|
# with strictDeps they must be nativeBuildInputs or alsa-sys cannot
|
||||||
# find pkg-config. alsa-lib is the actual linked library. cmake
|
# find pkg-config. alsa-lib and libopus are the actual linked
|
||||||
# builds the bundled libopus (opusic-sys, via the `opus` feature) —
|
# libraries: `headlessFeatures` leaves `opus-bundled` off, so
|
||||||
# deliberately without ninja, whose mere presence flips cmake's
|
# opusic-sys emits a plain `-lopus` instead of compiling the
|
||||||
# generator and then clashes with a cached build dir.
|
# vendored copy with cmake, and nixpkgs' libopus satisfies it.
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
pkgs.protobuf
|
pkgs.protobuf
|
||||||
pkgs.pkg-config
|
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
|
# audio-player links libasound; yt-dlp for /youtube stays a runtime
|
||||||
# dependency, not wired here.
|
# dependency, not wired here.
|
||||||
};
|
};
|
||||||
|
|
@ -159,6 +163,20 @@
|
||||||
// {
|
// {
|
||||||
cargoArtifacts = nativeDeps;
|
cargoArtifacts = nativeDeps;
|
||||||
doCheck = false; # tests need a config/network; `nix flake check` can run them later
|
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
|
# Build scripts / proc-macros compile for the build host; deps for the
|
||||||
# target use the cross toolchain. protoc, pkg-config, and cmake (for
|
# target use the cross toolchain. protoc, pkg-config, and cmake are
|
||||||
# the bundled libopus) are host tools.
|
# 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 = [
|
nativeBuildInputs = [
|
||||||
pkgs.protobuf
|
pkgs.protobuf
|
||||||
pkgs.pkg-config
|
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
|
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
|
next RPC cannot be forgotten. A count copied out of a file is not a check
|
||||||
against that file.
|
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