From cd3a16f95c0b481bde89c149ca782d1cfa0fb0c9 Mon Sep 17 00:00:00 2001 From: Test User Date: Sat, 25 Jul 2026 03:18:41 +0200 Subject: [PATCH] docs, flake: document tailored builds and name the flake features flake.nix first: its native package passed a bare --no-default-features, which used to mean "everything but web-ui" and now means *no providers at all*. It names its set explicitly (all-providers,opus,spectrum,notifications); the aarch64 cross build keeps the full defaults and its staged wasm bundle. docs/src/build-features.md: the feature table with what each one costs to lose, why fs takes /crabidy, /orphans, queue persistence and scan with it, the opus/libopus build note, two worked examples, what is deliberately not gated, and check-features. Linked from SUMMARY.md, and config.md now says the providers list can only offer what the binary was built with. README gains a short pointer. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 18 +++++ docs/src/SUMMARY.md | 1 + docs/src/build-features.md | 135 +++++++++++++++++++++++++++++++++++++ docs/src/config.md | 7 ++ flake.nix | 15 +++-- plan/build-features.md | 50 +++++++------- plan/summary.md | 78 +++++++++++++++++++++ 7 files changed, 275 insertions(+), 29 deletions(-) create mode 100644 docs/src/build-features.md diff --git a/README.md b/README.md index e23f6e5..fbc9502 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,24 @@ cargo run -p cbd # server + TUI in one process Or run the halves separately: `cargo run -p crabidy-server` and, in another terminal, `cargo run -p cbd-tui`. +### Tailored builds + +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: + +```sh +# a local-files appliance: no network providers, no web UI, no FFT +cargo build --release -p crabidy-server --no-default-features --features fs,opus +``` + +`crabidy-server features` prints what a binary was built with. See +[docs/src/build-features.md](docs/src/build-features.md) for the full +table (and what `fs` takes with it), and +[architecture/build-features.md](architecture/build-features.md) for the +design. + ## Configuration All configuration lives in `~/.config/crabidy/` (the platform config diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 549e331..b832c9a 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -17,4 +17,5 @@ - [The cbd bundle](./clients/cbd.md) - [Command line](./clients/cli.md) - [Configuration](./config.md) +- [Tailored builds](./build-features.md) - [Roles and authorization](./auth.md) diff --git a/docs/src/build-features.md b/docs/src/build-features.md new file mode 100644 index 0000000..8a265bb --- /dev/null +++ b/docs/src/build-features.md @@ -0,0 +1,135 @@ +# Tailored builds + + + +Every provider, Opus decoding, the spectrum bars, the embedded web UI and +the TUI's desktop notifications sit behind a Cargo **feature**. All of them +are **on by default**, so a plain `cargo build` gives you the full player. +Turning some off gives you a smaller binary that pulls fewer dependencies — +useful for a single-purpose box (a Raspberry Pi playing a local flac +collection) or a build environment you want to keep lean. + +This is the *compile-time* half of provider selection. The runtime half — +the `providers` list in `crabidy-server.toml` — still works exactly as +before; see [Configuration](./config.md#enabling-and-disabling-providers). +The two compose in one direction: **a provider that is not compiled in +cannot be enabled in the config**, and if you name one anyway the server +logs a warning and carries on. + +## What this build has + +```sh +crabidy-server features # or: cbd features +``` + +prints one feature per line — the providers it can mount, then the extras. +The same list goes into the server's startup log, so a support question +("why is `/tidal` missing?") is answerable from the binary and its log. + +## 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 | +| `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) | + +Plus two conveniences: `all-providers` enables the seven provider features +at once, and `cbd` (the bundle) mirrors every feature above and adds +`notifications` for the TUI's desktop "now playing" popups (`notify-rust`, +which on Linux pulls a D-Bus stack). + +### `fs` is more than `/fs` + +The `/fs` provider, the content store, and everything built on it are all +written against the same crate, so they share one feature. With `fs` off you +lose: + +- the `/fs` mount (your local music folder); +- `/crabidy` — saved queues, bookmarks (`w`) and captures (`W`); +- `/orphans`, which is a view over the store; +- **queue persistence**: the queue lives in memory, so a restart starts + empty; +- the `scan` command (it fails with a message naming the feature); +- captured-track markers in library listings. + +The capture and save-queue RPCs answer `Unimplemented` on such a server, so +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 + +`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. + +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. + +### `spectrum` and `web-ui` + +`spectrum` only affects the *server*: with it off no frames are computed or +broadcast, and clients simply show no bars — nothing else changes, and no +client needs rebuilding. `web-ui` drops the embedded browser client; the +gRPC service on port 50051 still serves `cbd-tui` and the CLI. + +## Examples + +A local-files appliance — no network providers, no web UI, no FFT: + +```sh +cargo build --release -p crabidy-server \ + --no-default-features --features fs,opus +``` + +A streaming box with the browser client and the bars, no local library: + +```sh +cargo build --release -p crabidy-server \ + --no-default-features --features tidal,web-ui,opus,spectrum +``` + +The bundle, tailored the same way (its features forward to the server): + +```sh +cargo build --release -p cbd --no-default-features --features fs,opus +``` + +A terminal client with no D-Bus dependency: + +```sh +cargo build --release -p cbd-tui --no-default-features +``` + +`--no-default-features` on its own is legal and compiles: you get a server +that starts, serves an empty library and plays nothing. It is the base case +the feature matrix checks, not a useful deployment. + +## What is *not* behind a feature + +- **Authorization.** `[auth]` and its password hashing always ship. A build + that ignored configured role hashes would silently run an + intended-to-be-locked server open — a fail-open hole not worth a small + dependency. +- **Audio output.** The server is the player; a server without audio has no + purpose. +- **HLS streaming and the spectrum tap.** They bring no dependencies of + their own, so gating them would add build complexity and save nothing. + +## Checking combinations + +`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. diff --git a/docs/src/config.md b/docs/src/config.md index 0b0894b..a29ff8f 100644 --- a/docs/src/config.md +++ b/docs/src/config.md @@ -47,6 +47,13 @@ left unread. Deleting the whole `providers` line re-enables everything (the same as a fresh install with no file). Because `orphans` is a view over the store, disabling `crabidy` disables `orphans` too. +The list can only offer what the binary was **built** with: providers are +also selectable at compile time (see [Tailored +builds](./build-features.md)). The default list written on first start +therefore names only the providers this build has, and if you add one it +does not have, the server logs a warning at startup and ignores it. Run +`crabidy-server features` to see what a binary contains. + ## Client config: `cbd-tui.toml` and `cbd.toml` `cbd-tui` (the standalone terminal client) reads `cbd-tui.toml`; `cbd` diff --git a/flake.nix b/flake.nix index 06d1176..95b0d9f 100644 --- a/flake.nix +++ b/flake.nix @@ -54,11 +54,18 @@ }; # Only the real binary crates; never build cbd-web (a wasm crate) for a - # host or aarch64 target. Headless: --no-default-features drops the - # embedded web UI. The aarch64 server (below) re-enables it and stages - # the wasm bundle. + # host or aarch64 target. workspaceBins = "-p crabidy-server -p cbd -p cbd-tui"; + # Headless: everything except the embedded web UI. Spelled out rather + # than "--no-default-features" alone, which since + # architecture/build-features.md means *no providers at all* — the + # features must be named 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. + headlessFeatures = "all-providers,opus,spectrum,notifications"; + # --- Web UI (wasm) bundle --------------------------------------------- # cbd-web is compiled to wasm by trunk; the output dist/ is what # crabidy-server's build.rs embeds (feature `web-ui`). The bundle is @@ -130,7 +137,7 @@ nativeArgs = { inherit src; strictDeps = true; - cargoExtraArgs = "--locked --no-default-features ${workspaceBins}"; + 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. diff --git a/plan/build-features.md b/plan/build-features.md index 68728c6..14d840e 100644 --- a/plan/build-features.md +++ b/plan/build-features.md @@ -13,24 +13,24 @@ devenv shell -- bash -lc 'CARGO_TARGET_DIR="$(pwd)/target-claude" cargo … ' ## A — Mount registry (behaviour-preserving refactor) -- [ ] **A1 — `Mount` + registry types.** In `provider.rs`: `struct Mount { +- [x] **A1 — `Mount` + registry types.** In `provider.rs`: `struct Mount { root: &'static str, name: &'static str, client: Arc }`, `Mount::new(root, name, Arc) -> Mount` (the `Arc` coerces at the call site), and `Mount::owns(&self, path) -> bool` matching `path == root || path.starts_with("/")` — so `/fsx` is not `/fs`. *Verifies:* `mount_owns_its_root_and_children_only` (G10). -- [ ] **A2 — `ProviderOrchestrator` holds `mounts: Vec`.** Replace the +- [x] **A2 — `ProviderOrchestrator` holds `mounts: Vec`.** Replace the nine `*_client` fields (keep `crabidy_store`). Add `from_mounts(Vec) -> Self` (sorts the mounts crabidy-first / orphans-last / rest alphabetical, and creates the bounded channel), used by `build` and the tests. *Verifies:* `root_lists_mounted_providers_in_order` (G12). -- [ ] **A3 — One dispatch helper.** `owner(&self, path) -> Option<&dyn +- [x] **A3 — One dispatch helper.** `owner(&self, path) -> Option<&dyn ProviderClient>` plus `owner_or(&self, path, err)` that warns with the same message and returns the typed error. Delete the nine `*_owns` functions and the nine `*_provider` accessors. *Verifies:* `unowned_paths_are_typed_errors` (G11). -- [ ] **A4 — Rewrite the eight dispatch methods** (`is_track_path`, +- [x] **A4 — Rewrite the eight dispatch methods** (`is_track_path`, `get_urls_for_track`, `get_metadata_for_track`, `get_lib_node`, `create_lib_node`, `rename_lib_node`, `delete_lib_node`, `resolve_tracks_into`) as a single lookup each, preserving: the synthetic @@ -39,35 +39,35 @@ devenv shell -- bash -lc 'CARGO_TARGET_DIR="$(pwd)/target-claude" cargo … ' `warn!`s. *Verifies:* `dispatch_reaches_the_owning_mount`, `overridden_resolve_tracks_into_is_dispatched`, `no_mounts_serves_an_empty_root` (G10, G11, G13). -- [ ] **A5 — `get_lib_root` maps the sorted mounts** to `LibraryNodeChild::new` +- [x] **A5 — `get_lib_root` maps the sorted mounts** to `LibraryNodeChild::new` (no per-provider block, no re-sort). *Verifies:* `root_lists_mounted_providers_in_order` (G12). -- [ ] **A6 — `build()` registers mounts.** Each provider's init block stays as +- [x] **A6 — `build()` registers mounts.** Each provider's init block stays as is; on success it pushes a `Mount` instead of assigning a field. `/crabidy` and `/orphans` push after the store exists. *Verifies:* full test suite + `cargo clippy -- -D warnings` (G2). -- [ ] **A7 — Commit A.** `cargo test -p crabidy-server`, clippy, fmt all clean; +- [x] **A7 — Commit A.** `cargo test -p crabidy-server`, clippy, fmt all clean; no `Cargo.toml` change in this commit (G1/G2 hold trivially). ## B — The features -- [ ] **B1 — Manifests** (done in api-design; re-verify): `crabidy-server` +- [x] **B1 — Manifests** (done in api-design; re-verify): `crabidy-server` (`default = all-providers + opus + spectrum + web-ui`, gated deps optional), `audio-player` (`opus`), `cbd-tui` (`notifications`), `cbd` (pass-through of all of them). *Verifies:* G4, G5; `cargo tree -p crabidy-server` at defaults matches the pre-change tree (G1). -- [ ] **B2 — `settings`: `BUILT_IN_PROVIDERS`, `provider_enabled` bounded by +- [x] **B2 — `settings`: `BUILT_IN_PROVIDERS`, `provider_enabled` bounded by it, `unavailable_providers()`, `ensure_default` writing only built-ins.** Replace the `todo!()`. *Verifies:* `an_absent_providers_key_enables_every_built_in_provider`, `unavailable_providers_reports_built_out_and_unknown_names`, `ensure_default_lists_only_built_in_providers`, `a_built_out_provider_cannot_be_enabled` (G4, G25, G26). -- [ ] **B3 — Per-provider `#[cfg]` in `build()` only.** Wrap each provider's +- [x] **B3 — Per-provider `#[cfg]` in `build()` only.** Wrap each provider's init+registration block in `#[cfg(feature = "…")]`; the `enabled.*` toggle read stays inside the block. Provider `use`/imports move to the same gate. *Verifies:* the matrix (B12) compiles each provider alone; G9 by reading. -- [ ] **B4 — `fs`: gate the store half.** `#[cfg(feature = "fs")]` on the +- [x] **B4 — `fs`: gate the store half.** `#[cfg(feature = "fs")]` on the `crabidy_store`, `capture`, and `orphans` modules (`lib.rs`), on `ProviderOrchestrator::crabidy_store`/its field, on `Playback`'s store field and `restore_current` and the persister, and on the `/fs`, `/crabidy`, @@ -75,66 +75,66 @@ devenv shell -- bash -lc 'CARGO_TARGET_DIR="$(pwd)/target-claude" cargo … ' parameter becomes `#[cfg]`-free by threading `Option>` only under the feature (adjust the two call sites in `lib.rs` and the playback tests). *Verifies:* G14, G17; `--no-default-features` build (B12). -- [ ] **B5 — `fs`: RPC degradation.** Without `fs`, `CaptureLibraryNode` and +- [x] **B5 — `fs`: RPC degradation.** Without `fs`, `CaptureLibraryNode` and `SaveQueue` return `Status::unimplemented` naming the feature; with `fs`, unchanged. No panic, no hang. *Verifies:* G15 by reading + a `--no-default-features` smoke run. -- [ ] **B6 — `fs`: the `scan` command.** Gate `AUDIO_EXTENSIONS`, +- [x] **B6 — `fs`: the `scan` command.** Gate `AUDIO_EXTENSIONS`, `is_audio_file`, `scan`, `scan_dir`, `scan_file` and their tests behind `fs`; without it the dispatcher in `crabidy-server/src/main.rs` and `cbd/src/main.rs` prints "this binary was built without the `fs` feature" and exits non-zero. The clap surface does not change. *Verifies:* G16, G9. -- [ ] **B7 — `opus` in `audio-player`.** Gate `mod opus_source` and the +- [x] **B7 — `opus` in `audio-player`.** Gate `mod opus_source` and the `is_ogg_opus` sniff in `player_engine::build_source`; without the feature an Ogg-Opus header produces `Err` naming the missing feature (the existing "failed to decode" path), never a panic. *Verifies:* G18, G19 (read + the `opus`-off matrix entry); `opus_source`'s own tests stay under the feature. -- [ ] **B8 — `opus` in `scan`.** `AUDIO_EXTENSIONS` includes `"opus"` only +- [x] **B8 — `opus` in `scan`.** `AUDIO_EXTENSIONS` includes `"opus"` only under the feature. *Verifies:* `opus_is_scannable_only_with_the_decoder`, `scan_walks_past_opus_files_it_cannot_play` (G20). -- [ ] **B9 — `spectrum`.** Gate `mod spectrum`, `spawn_spectrum_task`, and its +- [x] **B9 — `spectrum`.** Gate `mod spectrum`, `spawn_spectrum_task`, and its call; `realfft` optional. `Player::spectrum_tap()` unchanged. *Verifies:* G21; the `spectrum`-off matrix entry. -- [ ] **B10 — `notifications` in `cbd-tui`.** Gate the `notify_rust` import and +- [x] **B10 — `notifications` in `cbd-tui`.** Gate the `notify_rust` import and the notify call in `app/now_playing.rs`; the config key stays accepted so an existing `cbd-tui.toml` still parses. *Verifies:* G23; `cbd-tui --no-default-features` build + `cargo test -p cbd-tui`. -- [ ] **B11 — `features` command.** `cli::build_features()` + +- [x] **B11 — `features` command.** `cli::build_features()` + `cli::features()` (replace the `todo!()`s), wired into `ServerCommand::Features` and `CbdCommand::Features`; plus one `info!` line at startup listing the build, and one `warn!` per `unavailable_providers()` entry. *Verifies:* `build_features_lists_providers_then_extras` (G24, G25). -- [ ] **B12 — The build matrix as a devenv script.** `check-features` runs +- [x] **B12 — The build matrix as a devenv script.** `check-features` runs `cargo clippy -- -D warnings` (and `cargo test` where meaningful) over: defaults · `--no-default-features` · each of the seven providers alone · defaults-minus-`opus` · minus-`spectrum` · minus-`web-ui` · `-p cbd-tui --no-default-features` · `-p audio-player --no-default-features`. Add `cargo-hack` to devenv for ad-hoc deeper sweeps. *Verifies:* G6, G7. -- [ ] **B13 — Commit B** once the whole matrix is green and no +- [x] **B13 — Commit B** once the whole matrix is green and no `todo!()`/`unimplemented!()` from the stub stage remains (G28). ## C — Packaging and docs -- [ ] **C1 — `flake.nix`.** Native build: replace the bare +- [x] **C1 — `flake.nix`.** Native build: replace the bare `--no-default-features` with an explicit `--no-default-features --features all-providers,opus,spectrum`; leave the aarch64 cross build on defaults (`web-ui` on). *Verifies:* G8; `nix build .#crabidy` if the sandbox allows, otherwise read + the equivalent cargo invocation. -- [ ] **C2 — `docs/src/build-features.md`** — the feature table, what each one +- [x] **C2 — `docs/src/build-features.md`** — the feature table, what each one costs to lose, the two worked examples (local-files appliance: `--no-default-features --features fs,opus`; streaming box: `--no-default-features --features tidal,web-ui,opus,spectrum`), the compile-time vs runtime distinction, and the `features` command. Linked from `SUMMARY.md`; `docs/src/config.md` cross-references it from the `providers` key. *Verifies:* G27 + `mdbook build`. -- [ ] **C3 — `README.md`** gains a short "tailored builds" pointer. +- [x] **C3 — `README.md`** gains a short "tailored builds" pointer. *Verifies:* G27. -- [ ] **C4 — `plan/summary.md`** entry describing what shipped, the decisions +- [x] **C4 — `plan/summary.md`** entry describing what shipped, the decisions taken autonomously, and what stayed out (auth, HLS, spectrum tap — with the reason). *Verifies:* dev-flow convention. -- [ ] **C5 — Commit C.** +- [x] **C5 — Commit C.** ## Deferred / not done (recorded, not silently dropped) diff --git a/plan/summary.md b/plan/summary.md index 333bef3..3fe942c 100644 --- a/plan/summary.md +++ b/plan/summary.md @@ -1201,3 +1201,81 @@ covering enter/step/jump/back-sweep/exit/Esc/non-move-exit/node-focus-exit/ is_queable/filter); clippy `-D warnings` and fmt clean; markdownlint clean. Not exercised: live in-terminal keypresses (no TTY here) — the pure dispatch/table logic is fully unit-tested and the render path compiles. + +## build features (2026-07-25) + +Guarded each provider, Opus decoding, the spectrum, and desktop +notifications behind Cargo features (all default-on) so people can build a +non-bloated binary, with the dependency graph aligned to the flags. Ran the +full dev-flow: `architecture/build-features.md` (D1–D11), +`quality/build-features.md` (G1–G30), `plan/build-features.md` (A/B/C). +Three commits. + +**A — mount registry** (`crabidy-server/src/provider.rs`). The orchestrator +held nine `Option>` fields and repeated the same +`if …_owns(path)` chain across eight `ProviderClient` methods. +`ProviderClient` is dyn-compatible (`init` carries `Self: Sized`), so it now +holds `mounts: Vec` of `Arc` and every method is +one `owner(path)` lookup — ~600 lines of repetition gone, and a provider is +named in exactly one place, which is what made the feature work tractable. +Ordering (crabidy first, orphans last, rest alphabetical) moved to +`from_mounts`; the five config-file providers share a `mount_from_config` +helper. Behaviour-preserving, landed on its own with 6 new unit tests +(ownership boundaries, dispatch, typed errors for unowned paths, ordering, +the empty registry, dyn dispatch of an overridden `resolve_tracks_into`). + +**B — the features.** `crabidy-server`: `tidal`, `youtube`, `fyyd`, `abs`, +`soundcloud`, `jamendo`, `fs`, `opus`, `spectrum`, `web-ui`, plus +`all-providers`; `cbd` forwards all of them and adds `notifications`; +`cbd-tui` owns `notifications`; `audio-player` owns `opus`. Gated deps are +`optional = true`, and the three feature-carrying local crates get +`default-features = false` in `[workspace.dependencies]` (a member cannot +drop an inherited default). + +**Answers to the two questions in the request:** `scan` *did* already treat +`.opus` as playable; that entry is now conditional on `opus`, so a build +that cannot decode Opus will not index it either. And `fs` off does take +`/crabidy` and `/orphans` with it. + +Decisions taken autonomously (dev-flow, no questions): + +- **A feature must pay for itself in dependencies.** That is why `crabidy` + and `orphans` are *not* separate features (no dependency of their own — + `crabidy_store`/`capture`/`orphans` are written against `fsdy`), and why + `hls.rs`, `windowed_http.rs`, and `spectrum_tap.rs` stay unconditional. + Finer-grained control already exists at runtime in the `providers` list. +- **`fs` is one coherent unit**: `/fs`, the content store, `/crabidy`, + `/orphans`, bookmarks/captures, queue persistence, and `scan`. Without it + `CaptureLibraryNode`/`SaveQueue` answer `Unimplemented` and `scan` reports + the missing feature — no panics, no hangs. +- **`[auth]`/argon2 is deliberately not gated** (D9): a build that ignored + configured role hashes would run an intended-to-be-locked server open. + Refused as a fail-open hole for a small dependency. +- **The compile-time set bounds the runtime one**: `BUILT_IN_PROVIDERS` + filters `provider_enabled`, the auto-written default config lists only + built-ins, and a `providers` entry this binary lacks logs one warning + (fail-open, unlike auth). `crabidy-server features` / `cbd features` print + the compiled set and startup logs it. +- **An internal `_any-provider` marker feature** (every provider enables it) + expresses "is any provider compiled in?", which Cargo features cannot; + it keeps the legal-but-degenerate zero-provider build warning-free without + blanket `allow`s. +- **`flake.nix` had to change in the same breath**: its native package + passed a bare `--no-default-features` (meaning "all but `web-ui`"), which + after this work would have shipped a provider-*less* binary. Now + `--features all-providers,opus,spectrum,notifications`. + +Verification: `devenv shell -- check-features` (new script) runs the curated +matrix — defaults, `--no-default-features`, each of the seven providers +alone, each extra dropped, the two documented examples, `audio-player`, +`cbd-tui`, `cbd` — all clippy-clean under `-D warnings`, with tests at both +extremes. Full default suite green (88 server + 90 tui + the rest). **G1 +verified mechanically**: the default `cargo tree -p crabidy-server` set is +identical to the pre-change tree. Confirmed by inspection of `cargo tree` +that `--no-default-features` drops every gated crate, `opus`-off drops +`opusic-sys`/`symphonia-adapter-libopus`, and `cbd-tui +--no-default-features` drops `notify-rust`. Smoke-ran both the +provider-less and the `fs,opus` binaries under isolated XDG dirs: startup +logs the feature list, writes a default config listing only built-ins, and +warns once per unavailable name. Not exercised: `nix build` (not run here) +and playback of an Opus file in an `opus`-less build (no audio device).