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.