From b6b4ec04c159edcd9cd49a2c3873b8d5596b61f1 Mon Sep 17 00:00:00 2001
From: Michel de Graaf
Date: Mon, 21 Sep 2026 12:15:43 +0200
Subject: [PATCH] docs: install by system, stable and nightly
---
README.md | 61 +++++++-
website/src/lib/links.ts | 4 +
website/src/pages/install-routes.tsx | 218 +++++++++++++++++++++++++++
website/src/pages/install.tsx | 100 ++----------
4 files changed, 290 insertions(+), 93 deletions(-)
create mode 100644 website/src/pages/install-routes.tsx
diff --git a/README.md b/README.md
index db9eb9d..bf7808b 100644
--- a/README.md
+++ b/README.md
@@ -107,17 +107,66 @@ numbered release. A failed download, checksum, or startup check leaves an
existing binary untouched. Windows archives for both channels are on the
[releases page](https://github.com/michel/soulseek-rs/releases).
-The other stable routes need no installer script:
+Or install it with a package manager. Stable follows the latest release:
```bash
-brew install michel/tap/soulseek-rs # macOS and Linux
-cargo install soulseek-rs # or from crates.io
+# Debian, Ubuntu, Mint (on ARM: arm64 for amd64)
+curl -fsSLO https://github.com/michel/soulseek-rs/releases/latest/download/soulseek-rs-amd64.deb
+sudo apt install ./soulseek-rs-amd64.deb
+
+# Fedora, RHEL (on ARM: aarch64 for x86_64)
+sudo dnf install https://github.com/michel/soulseek-rs/releases/latest/download/soulseek-rs-x86_64.rpm
+
+# Nix
+nix run github:michel/soulseek-rs/master
+
+# Homebrew, on macOS and Linux
+brew install michel/tap/soulseek-rs
+
+# cargo-binstall fetches the release binary and compiles nothing
+cargo binstall soulseek-rs
+
+# From source: Rust 1.91 or newer. Debian 13 and Ubuntu 24.04 default to an older one
+cargo install --locked soulseek-rs
```
+Nightly is the tip of `develop`. The `.deb` and `.rpm` are downloads; Nix,
+Homebrew and cargo build it from source:
+
+```bash
+curl -fsSLO https://github.com/michel/soulseek-rs/releases/download/nightly/soulseek-rs-nightly-amd64.deb
+sudo apt install ./soulseek-rs-nightly-amd64.deb
+
+sudo dnf install https://github.com/michel/soulseek-rs/releases/download/nightly/soulseek-rs-nightly-x86_64.rpm
+
+nix run github:michel/soulseek-rs/develop
+
+brew install --HEAD michel/tap/soulseek-rs
+
+cargo install --locked --git https://github.com/michel/soulseek-rs --branch develop soulseek-rs
+```
+
+A `.deb` or `.rpm` does not update itself. Install the newer file over it. A
+nightly sorts above the release it follows, so going back to stable is a
+downgrade. In a script apt refuses one without `--allow-downgrades`, while dnf
+goes ahead. Both carry the man page and completions for bash, zsh and fish.
+A release download prints the plain number, `19.0.0`. Any Nix build, and a
+nightly download, reads `19.0.0+git202609141144.deb846b`: the last release, the
+commit's time in UTC, and the commit. A Homebrew `--HEAD` build reports
+`HEAD-deb846b`. A `cargo install` from git sets no version, so it prints
+`19.0.0` too.
+
From source, `cargo build --release` leaves the binary at
`target/release/soulseek-rs`. To build your own client on the protocol library,
run `cargo add soulseek-rs-lib`.
+To package it for a distribution: a musl build is static, a glibc build links
+only libc, libm and libgcc_s,
+`soulseek-rs completions print ` and `soulseek-rs man` write the
+completion scripts and the man page to stdout, and a build with
+`SOULSEEK_RS_VERSION` set reports that instead of the crate version. The nfpm
+recipe behind the `.deb` and `.rpm` is in [`packaging/`](packaging).
+
## Usage
Run it with no subcommand for the interactive TUI:
@@ -332,6 +381,8 @@ soulseek-rs config path|list|get|set
soulseek-rs portmap # test automatic port mapping
soulseek-rs skills install|uninstall|list # teach a coding agent this CLI
soulseek-rs completions install|uninstall # tab completion for bash/zsh/fish
+soulseek-rs completions print # the script on stdout, for packaging
+soulseek-rs man # the man page on stdout
```
A script usually starts with `whoami`, which answers "are these credentials
@@ -343,9 +394,9 @@ download folder. `--no-daemon` tests this machine's own login instead.
`shares list` reads the config file either way, while `shares status` and
`shares reindex` ask the session and report what peers can see.
-Twenty need no credentials: `config path|list|get|set`,
+Twenty-two need no credentials: `config path|list|get|set`,
`shares list|add|remove`, `wish add|remove|list`, `portmap`,
-`skills install|uninstall|list`, `completions install|uninstall`,
+`skills install|uninstall|list`, `completions install|uninstall|print`, `man`,
`account logout` and `daemon token|status|stop`. Most never touch the network; `daemon status` and
`daemon stop` talk to a daemon that is already logged in, so a machine with no
account of its own can still ask whether one is running. `shares status` and
diff --git a/website/src/lib/links.ts b/website/src/lib/links.ts
index fdb4f37..54385ed 100644
--- a/website/src/lib/links.ts
+++ b/website/src/lib/links.ts
@@ -29,3 +29,7 @@ export const INSTALL_CMD = `curl -fsSL ${SITE_URL}install.sh | sh`
export const NIGHTLY_INSTALL_CMD =
'curl -fsSL https://raw.githubusercontent.com/michel/soulseek-rs/develop/website/public/install.sh | sh -s -- --nightly'
+
+export const STABLE_DOWNLOAD = `${LINKS.releases}/latest/download`
+
+export const NIGHTLY_DOWNLOAD = `${LINKS.releases}/download/nightly`
diff --git a/website/src/pages/install-routes.tsx b/website/src/pages/install-routes.tsx
new file mode 100644
index 0000000..2aa1ffc
--- /dev/null
+++ b/website/src/pages/install-routes.tsx
@@ -0,0 +1,218 @@
+import { ExtLink } from '@/components/ui/ext-link'
+import { Code } from '@/components/ui/inline-code'
+import { OsIcon, type OsName } from '@/components/ui/os-icon'
+import { Terminal, type TermLine } from '@/components/ui/terminal'
+import { LINKS, NIGHTLY_DOWNLOAD, STABLE_DOWNLOAD } from '@/lib/links'
+
+interface InstallRoute {
+ id: string
+ os: OsName
+ best: string
+ lines: readonly TermLine[]
+ alt: React.ReactNode
+}
+
+// aislop-ignore-file code-quality/duplicate-block -- data tables: the INSTALLS and LINUX_ROUTES records repeat the field names their interfaces require. The rule matches punctuation, not logic.
+const INSTALLS: readonly InstallRoute[] = [
+ {
+ id: 'macos',
+ os: 'macOS',
+ best: 'Homebrew',
+ lines: [
+ { t: 'cmd', text: 'brew install michel/tap/soulseek-rs' },
+ { t: 'cmd', text: 'soulseek-rs' },
+ ],
+ alt: (
+ <>
+ A prebuilt binary for Apple silicon and Intel, no Rust toolchain needed. With a
+ toolchain, cargo install --locked soulseek-rs builds from crates.io instead.
+ >
+ ),
+ },
+ {
+ id: 'linux',
+ os: 'Linux',
+ best: 'your package manager',
+ lines: [
+ { t: 'cm', text: '# Debian and Ubuntu; the others are below' },
+ { t: 'cmd', text: `curl -fsSLO ${STABLE_DOWNLOAD}/soulseek-rs-amd64.deb` },
+ { t: 'cmd', text: 'sudo apt install ./soulseek-rs-amd64.deb' },
+ { t: 'cmd', text: 'soulseek-rs' },
+ ],
+ alt: (
+ <>
+ Fedora, Nix, Homebrew and cargo are{' '}
+
+ listed below
+
+ , each with a nightly. Without any of them, the{' '}
+ releases page has static musl archives for x86-64
+ and arm64 that run on any distribution.
+ >
+ ),
+ },
+ {
+ id: 'windows',
+ os: 'Windows',
+ best: 'cargo or a prebuilt .exe',
+ lines: [
+ { t: 'cmd', text: 'cargo install --locked soulseek-rs' },
+ { t: 'cmd', text: 'soulseek-rs.exe' },
+ ],
+ alt: (
+ <>
+ Cargo needs the MSVC build tools. To skip both, take the{' '}
+ pc-windows-msvc zip from the{' '}
+ releases page, unpack it, and put{' '}
+ soulseek-rs.exe on your PATH.
+ >
+ ),
+ },
+]
+
+interface LinuxRoute {
+ id: string
+ name: string
+ stable: readonly TermLine[]
+ nightly: readonly TermLine[]
+ note: React.ReactNode
+}
+
+const LINUX_ROUTES: readonly LinuxRoute[] = [
+ {
+ id: 'deb',
+ name: 'Debian, Ubuntu, Mint',
+ stable: [
+ { t: 'cmd', text: `curl -fsSLO ${STABLE_DOWNLOAD}/soulseek-rs-amd64.deb` },
+ { t: 'cmd', text: 'sudo apt install ./soulseek-rs-amd64.deb' },
+ ],
+ nightly: [
+ { t: 'cmd', text: `curl -fsSLO ${NIGHTLY_DOWNLOAD}/soulseek-rs-nightly-amd64.deb` },
+ { t: 'cmd', text: 'sudo apt install ./soulseek-rs-nightly-amd64.deb' },
+ ],
+ note: (
+ <>
+ On ARM, arm64 takes the place of amd64. There is no apt
+ repository behind this, so an update means installing the newer file over the old one.
+ A nightly sorts above the release it follows, so going back to stable is a downgrade,
+ and apt asks before doing it.
+ >
+ ),
+ },
+ {
+ id: 'rpm',
+ name: 'Fedora, RHEL',
+ stable: [{ t: 'cmd', text: `sudo dnf install ${STABLE_DOWNLOAD}/soulseek-rs-x86_64.rpm` }],
+ nightly: [
+ { t: 'cmd', text: `sudo dnf install ${NIGHTLY_DOWNLOAD}/soulseek-rs-nightly-x86_64.rpm` },
+ ],
+ note: (
+ <>
+ On ARM, aarch64 takes the place of x86_64. Run the same
+ command again to update. dnf fetches the file each time, so a newer nightly at the same
+ address is picked up. An older file downgrades without asking.
+ >
+ ),
+ },
+ {
+ id: 'nix',
+ name: 'Nix',
+ stable: [{ t: 'cmd', text: 'nix run github:michel/soulseek-rs/master' }],
+ nightly: [{ t: 'cmd', text: 'nix run github:michel/soulseek-rs/develop' }],
+ note: (
+ <>
+ Both build from source. With no branch at all, Nix takes the repository’s
+ default one, and here that is develop.
+ >
+ ),
+ },
+ {
+ id: 'brew',
+ name: 'Homebrew',
+ stable: [{ t: 'cmd', text: 'brew install michel/tap/soulseek-rs' }],
+ nightly: [{ t: 'cmd', text: 'brew install --HEAD michel/tap/soulseek-rs' }],
+ note: (
+ <>
+ Homebrew installs under /home/linuxbrew, outside the image that a system
+ update replaces on Bazzite and Silverblue. --HEAD compiles{' '}
+ develop with Homebrew’s own Rust.
+ >
+ ),
+ },
+ {
+ id: 'cargo',
+ name: 'cargo',
+ stable: [{ t: 'cmd', text: 'cargo install --locked soulseek-rs' }],
+ nightly: [
+ {
+ t: 'cmd',
+ text: 'cargo install --locked --git https://github.com/michel/soulseek-rs --branch develop soulseek-rs',
+ },
+ ],
+ note: (
+ <>
+ Needs Rust 1.91 or newer. Debian 13 and Ubuntu 24.04 default to an older one, so take
+ the toolchain from rustup.rs.{' '}
+ cargo binstall soulseek-rs fetches the release binary and compiles nothing.
+ >
+ ),
+ },
+]
+
+export const InstallCards = () => (
+
+ {INSTALLS.map((route) => (
+
+
+
+
+ {route.os}
+
+
+
+ {route.best}
+
+
+
+ {route.alt}
+
+
+ ))}
+
+)
+
+export const LinuxRoutes = () => (
+
+
+ Linux, by distribution
+
+
+ Stable is the latest release, or for Nix the master branch it was cut
+ from. Nightly is the tip of develop, rebuilt every day. A release download
+ prints the plain number, 19.0.0. Any Nix build, and a nightly download,
+ reads 19.0.0+git202609141144.deb846b: the last release, the
+ commit’s time in UTC, and the commit. A Homebrew --HEAD build
+ reports HEAD-deb846b, and a cargo install from git sets no
+ version, so it prints 19.0.0 too. The .deb and .rpm
+ carry the man page and completions for bash, zsh and fish.
+
+ {LINUX_ROUTES.map((route) => (
+
+
+ {route.name}
+
+
+
+
+
+
+ {route.note}
+
+
+ ))}
+
+)
diff --git a/website/src/pages/install.tsx b/website/src/pages/install.tsx
index f17cf70..9e03fb0 100644
--- a/website/src/pages/install.tsx
+++ b/website/src/pages/install.tsx
@@ -3,10 +3,10 @@ import { FeatureCard } from '@/components/ui/feature-card'
import { ExtLink } from '@/components/ui/ext-link'
import { Code } from '@/components/ui/inline-code'
import { Cols, PageHead, Prose, Section, SectionHead } from '@/components/ui/layout'
-import { OsIcon, type OsName } from '@/components/ui/os-icon'
import { Callout } from '@/components/ui/panel'
import { Terminal, type TermLine } from '@/components/ui/terminal'
import { INSTALL_CMD, LINKS, NIGHTLY_INSTALL_CMD, SITE_URL } from '@/lib/links'
+import { InstallCards, LinuxRoutes } from '@/pages/install-routes'
const INSTALL_SCRIPT_LINES: readonly TermLine[] = [
{ t: 'cm', text: '# latest stable release' },
@@ -36,66 +36,6 @@ const LIB_LINES: TermLine[] = LIB_SRC.split('\n').map((text) => ({
text,
}))
-interface InstallRoute {
- id: string
- os: OsName
- best: string
- lines: readonly TermLine[]
- alt: React.ReactNode
-}
-
-// aislop-ignore-file code-quality/duplicate-block -- three findings here, all data tables: the INSTALLS records repeat the field names InstallRoute requires, and the two uninstall `lines` arrays repeat the `{ t, text }` shape around different commands. The rule matches punctuation, not logic.
-const INSTALLS: readonly InstallRoute[] = [
- {
- id: 'macos',
- os: 'macOS',
- best: 'Homebrew',
- lines: [
- { t: 'cmd', text: 'brew install michel/tap/soulseek-rs' },
- { t: 'cmd', text: 'soulseek-rs' },
- ],
- alt: (
- <>
- A prebuilt binary for Apple silicon and Intel, no Rust toolchain needed. With a
- toolchain, cargo install soulseek-rs builds from crates.io instead.
- >
- ),
- },
- {
- id: 'linux',
- os: 'Linux',
- best: 'cargo',
- lines: [
- { t: 'cmd', text: 'cargo install soulseek-rs' },
- { t: 'cmd', text: 'soulseek-rs' },
- ],
- alt: (
- <>
- Homebrew on Linux works the same as on macOS. Without either, the{' '}
- releases page has static musl archives for x86-64 and
- arm64 that run on any distribution.
- >
- ),
- },
- {
- id: 'windows',
- os: 'Windows',
- best: 'cargo or a prebuilt .exe',
- lines: [
- { t: 'cmd', text: 'cargo install soulseek-rs' },
- { t: 'cmd', text: 'soulseek-rs.exe' },
- ],
- alt: (
- <>
- Cargo needs the MSVC build tools. To skip both, take the{' '}
- pc-windows-msvc zip from the{' '}
- releases page, unpack it, and put{' '}
- soulseek-rs.exe on your PATH.
- >
- ),
- },
-]
-
interface PlatformNote {
id: string
name: string
@@ -291,32 +231,12 @@ const Steps = () => (
place.
- Or pick a route per platform. Cargo needs a Rust toolchain from{' '}
- rustup.rs; the other routes don’t.
+ Or pick a route per platform. Cargo needs Rust 1.91 or newer from{' '}
+ rustup.rs. Nix and Homebrew{' '}
+ --HEAD also build from source, but bring their own Rust.
-
- {INSTALLS.map((route) => (
-
-
-
-
- {route.os}
-
-
-
- {route.best}
-
-
-
- {route.alt}
-
-
- ))}
-
+
+
Every route puts the same binary on your PATH. Run soulseek-rs in
a terminal and the TUI opens, in a script or a pipe it wants a subcommand
@@ -522,11 +442,13 @@ const Config = () => (
)
+// aislop-ignore-file code-quality/duplicate-block -- the two uninstall `lines` arrays repeat the `{ t, text }` shape around different commands. The rule matches punctuation, not logic.
const Uninstall = () => (
Three things exist on disk: the binary, the config, and the state, plus the completion
- scripts and the agent skill if you asked for those. Nothing else, unless you point{' '}
+ scripts and the agent skill if you asked for those. A .deb or .rpm also brings a man page
+ and its own completions, and takes them back when removed. Nothing else, unless you point{' '}
--log-file somewhere. No telemetry to opt out of.
@@ -539,6 +461,8 @@ const Uninstall = () => (
{ t: 'cm', text: '# whichever way you installed it' },
{ t: 'cmd', text: 'brew uninstall soulseek-rs' },
{ t: 'cmd', text: 'cargo uninstall soulseek-rs' },
+ { t: 'cmd', text: 'sudo apt remove soulseek-rs' },
+ { t: 'cmd', text: 'sudo dnf remove soulseek-rs' },
{ t: 'cm', text: '# script install without brew: the binary itself' },
{ t: 'cmd', text: 'rm -f /usr/local/bin/soulseek-rs ~/.local/bin/soulseek-rs' },
{ t: 'cm', text: '# brew only: drop the tap it came from' },
@@ -581,7 +505,7 @@ const Uninstall = () => (
export const Install = () => (
<>
- One command with the install script, Homebrew or cargo. Build from source if
+ One command with the install script, your package manager, Homebrew or cargo. Build from source if
you’d rather. Or depend on the library and write your own client.