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) <noreply@anthropic.com>
This commit is contained in:
Test User 2026-07-25 03:18:41 +02:00
parent a03e3de84e
commit cd3a16f95c
7 changed files with 275 additions and 29 deletions

View File

@ -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 Or run the halves separately: `cargo run -p crabidy-server` and, in
another terminal, `cargo run -p cbd-tui`. 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 ## Configuration
All configuration lives in `~/.config/crabidy/` (the platform config All configuration lives in `~/.config/crabidy/` (the platform config

View File

@ -17,4 +17,5 @@
- [The cbd bundle](./clients/cbd.md) - [The cbd bundle](./clients/cbd.md)
- [Command line](./clients/cli.md) - [Command line](./clients/cli.md)
- [Configuration](./config.md) - [Configuration](./config.md)
- [Tailored builds](./build-features.md)
- [Roles and authorization](./auth.md) - [Roles and authorization](./auth.md)

135
docs/src/build-features.md Normal file
View File

@ -0,0 +1,135 @@
# Tailored builds
<!-- toc -->
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.

View File

@ -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 same as a fresh install with no file). Because `orphans` is a view over the
store, disabling `crabidy` disables `orphans` too. 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` ## Client config: `cbd-tui.toml` and `cbd.toml`
`cbd-tui` (the standalone terminal client) reads `cbd-tui.toml`; `cbd` `cbd-tui` (the standalone terminal client) reads `cbd-tui.toml`; `cbd`

View File

@ -54,11 +54,18 @@
}; };
# Only the real binary crates; never build cbd-web (a wasm crate) for a # Only the real binary crates; never build cbd-web (a wasm crate) for a
# host or aarch64 target. Headless: --no-default-features drops the # host or aarch64 target.
# embedded web UI. The aarch64 server (below) re-enables it and stages
# the wasm bundle.
workspaceBins = "-p crabidy-server -p cbd -p cbd-tui"; 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 --------------------------------------------- # --- Web UI (wasm) bundle ---------------------------------------------
# cbd-web is compiled to wasm by trunk; the output dist/ is what # 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 # crabidy-server's build.rs embeds (feature `web-ui`). The bundle is
@ -130,7 +137,7 @@
nativeArgs = { nativeArgs = {
inherit src; inherit src;
strictDeps = true; 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); # 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. # find pkg-config. alsa-lib is the actual linked library.

View File

@ -13,24 +13,24 @@ devenv shell -- bash -lc 'CARGO_TARGET_DIR="$(pwd)/target-claude" cargo … '
## A — Mount registry (behaviour-preserving refactor) ## 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<dyn ProviderClient> }`, root: &'static str, name: &'static str, client: Arc<dyn ProviderClient> }`,
`Mount::new(root, name, Arc<impl ProviderClient>) -> Mount` (the `Arc` `Mount::new(root, name, Arc<impl ProviderClient>) -> Mount` (the `Arc`
coerces at the call site), and `Mount::owns(&self, path) -> bool` matching coerces at the call site), and `Mount::owns(&self, path) -> bool` matching
`path == root || path.starts_with("<root>/")` — so `/fsx` is not `/fs`. `path == root || path.starts_with("<root>/")` — so `/fsx` is not `/fs`.
*Verifies:* `mount_owns_its_root_and_children_only` (G10). *Verifies:* `mount_owns_its_root_and_children_only` (G10).
- [ ] **A2 — `ProviderOrchestrator` holds `mounts: Vec<Mount>`.** Replace the - [x] **A2 — `ProviderOrchestrator` holds `mounts: Vec<Mount>`.** Replace the
nine `*_client` fields (keep `crabidy_store`). Add nine `*_client` fields (keep `crabidy_store`). Add
`from_mounts(Vec<Mount>) -> Self` (sorts the mounts crabidy-first / `from_mounts(Vec<Mount>) -> Self` (sorts the mounts crabidy-first /
orphans-last / rest alphabetical, and creates the bounded channel), used by orphans-last / rest alphabetical, and creates the bounded channel), used by
`build` and the tests. *Verifies:* `root_lists_mounted_providers_in_order` `build` and the tests. *Verifies:* `root_lists_mounted_providers_in_order`
(G12). (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 ProviderClient>` plus `owner_or(&self, path, err)` that warns with the same
message and returns the typed error. Delete the nine `*_owns` functions and message and returns the typed error. Delete the nine `*_owns` functions and
the nine `*_provider` accessors. *Verifies:* `unowned_paths_are_typed_errors` the nine `*_provider` accessors. *Verifies:* `unowned_paths_are_typed_errors`
(G11). (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`, `get_urls_for_track`, `get_metadata_for_track`, `get_lib_node`,
`create_lib_node`, `rename_lib_node`, `delete_lib_node`, `create_lib_node`, `rename_lib_node`, `delete_lib_node`,
`resolve_tracks_into`) as a single lookup each, preserving: the synthetic `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`, `warn!`s. *Verifies:* `dispatch_reaches_the_owning_mount`,
`overridden_resolve_tracks_into_is_dispatched`, `overridden_resolve_tracks_into_is_dispatched`,
`no_mounts_serves_an_empty_root` (G10, G11, G13). `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:* (no per-provider block, no re-sort). *Verifies:*
`root_lists_mounted_providers_in_order` (G12). `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` is; on success it pushes a `Mount` instead of assigning a field. `/crabidy`
and `/orphans` push after the store exists. *Verifies:* full test suite + and `/orphans` push after the store exists. *Verifies:* full test suite +
`cargo clippy -- -D warnings` (G2). `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). no `Cargo.toml` change in this commit (G1/G2 hold trivially).
## B — The features ## 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), (`default = all-providers + opus + spectrum + web-ui`, gated deps optional),
`audio-player` (`opus`), `cbd-tui` (`notifications`), `cbd` (pass-through of `audio-player` (`opus`), `cbd-tui` (`notifications`), `cbd` (pass-through of
all of them). *Verifies:* G4, G5; `cargo tree -p crabidy-server` at defaults all of them). *Verifies:* G4, G5; `cargo tree -p crabidy-server` at defaults
matches the pre-change tree (G1). 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.** it, `unavailable_providers()`, `ensure_default` writing only built-ins.**
Replace the `todo!()`. *Verifies:* Replace the `todo!()`. *Verifies:*
`an_absent_providers_key_enables_every_built_in_provider`, `an_absent_providers_key_enables_every_built_in_provider`,
`unavailable_providers_reports_built_out_and_unknown_names`, `unavailable_providers_reports_built_out_and_unknown_names`,
`ensure_default_lists_only_built_in_providers`, `ensure_default_lists_only_built_in_providers`,
`a_built_out_provider_cannot_be_enabled` (G4, G25, G26). `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 init+registration block in `#[cfg(feature = "…")]`; the `enabled.*` toggle
read stays inside the block. Provider `use`/imports move to the same gate. read stays inside the block. Provider `use`/imports move to the same gate.
*Verifies:* the matrix (B12) compiles each provider alone; G9 by reading. *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 `crabidy_store`, `capture`, and `orphans` modules (`lib.rs`), on
`ProviderOrchestrator::crabidy_store`/its field, on `Playback`'s store field `ProviderOrchestrator::crabidy_store`/its field, on `Playback`'s store field
and `restore_current` and the persister, and on the `/fs`, `/crabidy`, 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<Arc<CrabidyStore>>` parameter becomes `#[cfg]`-free by threading `Option<Arc<CrabidyStore>>`
only under the feature (adjust the two call sites in `lib.rs` and the only under the feature (adjust the two call sites in `lib.rs` and the
playback tests). *Verifies:* G14, G17; `--no-default-features` build (B12). 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`, `SaveQueue` return `Status::unimplemented` naming the feature; with `fs`,
unchanged. No panic, no hang. *Verifies:* G15 by reading + a unchanged. No panic, no hang. *Verifies:* G15 by reading + a
`--no-default-features` smoke run. `--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 `is_audio_file`, `scan`, `scan_dir`, `scan_file` and their tests behind
`fs`; without it the dispatcher in `crabidy-server/src/main.rs` and `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" `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. 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 `is_ogg_opus` sniff in `player_engine::build_source`; without the feature an
Ogg-Opus header produces `Err` naming the missing feature (the existing Ogg-Opus header produces `Err` naming the missing feature (the existing
"failed to decode" path), never a panic. *Verifies:* G18, G19 (read + the "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. `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`, under the feature. *Verifies:* `opus_is_scannable_only_with_the_decoder`,
`scan_walks_past_opus_files_it_cannot_play` (G20). `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:* call; `realfft` optional. `Player::spectrum_tap()` unchanged. *Verifies:*
G21; the `spectrum`-off matrix entry. 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 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 existing `cbd-tui.toml` still parses. *Verifies:* G23; `cbd-tui
--no-default-features` build + `cargo test -p 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 `cli::features()` (replace the `todo!()`s), wired into
`ServerCommand::Features` and `CbdCommand::Features`; plus one `info!` line `ServerCommand::Features` and `CbdCommand::Features`; plus one `info!` line
at startup listing the at startup listing the
build, and one `warn!` per `unavailable_providers()` entry. *Verifies:* build, and one `warn!` per `unavailable_providers()` entry. *Verifies:*
`build_features_lists_providers_then_extras` (G24, G25). `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: `cargo clippy -- -D warnings` (and `cargo test` where meaningful) over:
defaults · `--no-default-features` · each of the seven providers alone · defaults · `--no-default-features` · each of the seven providers alone ·
defaults-minus-`opus` · minus-`spectrum` · minus-`web-ui` · `-p cbd-tui defaults-minus-`opus` · minus-`spectrum` · minus-`web-ui` · `-p cbd-tui
--no-default-features` · `-p audio-player --no-default-features`. Add --no-default-features` · `-p audio-player --no-default-features`. Add
`cargo-hack` to devenv for ad-hoc deeper sweeps. *Verifies:* G6, G7. `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). `todo!()`/`unimplemented!()` from the stub stage remains (G28).
## C — Packaging and docs ## 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` with an explicit
`--no-default-features --features all-providers,opus,spectrum`; leave the `--no-default-features --features all-providers,opus,spectrum`; leave the
aarch64 cross build on defaults (`web-ui` on). *Verifies:* G8; aarch64 cross build on defaults (`web-ui` on). *Verifies:* G8;
`nix build .#crabidy` if the sandbox allows, otherwise read + the equivalent `nix build .#crabidy` if the sandbox allows, otherwise read + the equivalent
cargo invocation. 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: costs to lose, the two worked examples (local-files appliance:
`--no-default-features --features fs,opus`; streaming box: `--no-default-features --features fs,opus`; streaming box:
`--no-default-features --features tidal,web-ui,opus,spectrum`), the `--no-default-features --features tidal,web-ui,opus,spectrum`), the
compile-time vs runtime distinction, and the `features` command. Linked from compile-time vs runtime distinction, and the `features` command. Linked from
`SUMMARY.md`; `docs/src/config.md` cross-references it from the `providers` `SUMMARY.md`; `docs/src/config.md` cross-references it from the `providers`
key. *Verifies:* G27 + `mdbook build`. 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. *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 taken autonomously, and what stayed out (auth, HLS, spectrum tap — with the
reason). *Verifies:* dev-flow convention. reason). *Verifies:* dev-flow convention.
- [ ] **C5 — Commit C.** - [x] **C5 — Commit C.**
## Deferred / not done (recorded, not silently dropped) ## Deferred / not done (recorded, not silently dropped)

View File

@ -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 is_queable/filter); clippy `-D warnings` and fmt clean; markdownlint clean. Not
exercised: live in-terminal keypresses (no TTY here) — the pure dispatch/table exercised: live in-terminal keypresses (no TTY here) — the pure dispatch/table
logic is fully unit-tested and the render path compiles. 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` (D1D11),
`quality/build-features.md` (G1G30), `plan/build-features.md` (A/B/C).
Three commits.
**A — mount registry** (`crabidy-server/src/provider.rs`). The orchestrator
held nine `Option<Arc<ConcreteClient>>` 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<Mount>` of `Arc<dyn ProviderClient>` 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).