docs: install by system, stable and nightly

This commit is contained in:
Michel de Graaf
2026-09-21 12:58:09 +02:00
parent 4c5a808762
commit b6b4ec04c1
4 changed files with 290 additions and 93 deletions
+56 -5
View File
@@ -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 <shell>` 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 <SHELL> # 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
+4
View File
@@ -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`
+218
View File
@@ -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, <Code>cargo install --locked soulseek-rs</Code> 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{' '}
<a href="#linux-packages" className="text-link hover:text-link-hover">
listed below
</a>
, each with a nightly. Without any of them, the{' '}
<ExtLink href={LINKS.releases}>releases page</ExtLink> 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{' '}
<code>pc-windows-msvc</code> zip from the{' '}
<ExtLink href={LINKS.releases}>releases page</ExtLink>, unpack it, and put{' '}
<code>soulseek-rs.exe</code> 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, <code>arm64</code> takes the place of <code>amd64</code>. 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, <code>aarch64</code> takes the place of <code>x86_64</code>. 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&rsquo;s
default one, and here that is <code>develop</code>.
</>
),
},
{
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 <code>/home/linuxbrew</code>, outside the image that a system
update replaces on Bazzite and Silverblue. <code>--HEAD</code> compiles{' '}
<code>develop</code> with Homebrew&rsquo;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 <ExtLink href={LINKS.rustup}>rustup.rs</ExtLink>.{' '}
<code>cargo binstall soulseek-rs</code> fetches the release binary and compiles nothing.
</>
),
},
]
export const InstallCards = () => (
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
{INSTALLS.map((route) => (
<div
key={route.id}
id={route.id}
className="flex scroll-mt-20 flex-col gap-3.5 rounded-md border border-hairline bg-panel p-[18px] sm:p-[22px]"
>
<div className="flex items-center gap-2.5">
<OsIcon name={route.os} />
<h3 className="text-heading leading-[var(--text-heading--line-height)] font-medium">
{route.os}
</h3>
</div>
<span className="text-[11px] uppercase tracking-[var(--tracking-label)] text-secondary">
{route.best}
</span>
<Terminal lines={route.lines} wrap />
<p className="text-[12.5px] leading-5 text-secondary [&_code]:font-mono [&_code]:text-primary">
{route.alt}
</p>
</div>
))}
</div>
)
export const LinuxRoutes = () => (
<div id="linux-packages" className="flex scroll-mt-20 flex-col gap-5">
<h3 className="text-heading leading-[var(--text-heading--line-height)] font-medium">
Linux, by distribution
</h3>
<p className="text-secondary">
Stable is the latest release, or for Nix the <Code>master</Code> branch it was cut
from. Nightly is the tip of <Code>develop</Code>, rebuilt every day. A release download
prints the plain number, <Code>19.0.0</Code>. Any Nix build, and a nightly download,
reads <Code>19.0.0+git202609141144.deb846b</Code>: the last release, the
commit&rsquo;s time in UTC, and the commit. A Homebrew <Code>--HEAD</Code> build
reports <Code>HEAD-deb846b</Code>, and a <Code>cargo install</Code> from git sets no
version, so it prints <Code>19.0.0</Code> too. The .deb and .rpm
carry the man page and completions for bash, zsh and fish.
</p>
{LINUX_ROUTES.map((route) => (
<div key={route.id} className="flex flex-col gap-2.5">
<span className="text-[11px] uppercase tracking-[var(--tracking-label)] text-secondary">
{route.name}
</span>
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
<Terminal label="stable" lines={route.stable} wrap />
<Terminal label="nightly" lines={route.nightly} wrap />
</div>
<p className="text-[12.5px] leading-5 text-secondary [&_code]:font-mono [&_code]:text-primary">
{route.note}
</p>
</div>
))}
</div>
)
+12 -88
View File
@@ -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, <Code>cargo install soulseek-rs</Code> 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{' '}
<ExtLink href={LINKS.releases}>releases page</ExtLink> 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{' '}
<code>pc-windows-msvc</code> zip from the{' '}
<ExtLink href={LINKS.releases}>releases page</ExtLink>, unpack it, and put{' '}
<code>soulseek-rs.exe</code> on your PATH.
</>
),
},
]
interface PlatformNote {
id: string
name: string
@@ -291,32 +231,12 @@ const Steps = () => (
place.
</p>
<p className="text-secondary">
Or pick a route per platform. Cargo needs a Rust toolchain from{' '}
<ExtLink href={LINKS.rustup}>rustup.rs</ExtLink>; the other routes don&rsquo;t.
Or pick a route per platform. Cargo needs Rust 1.91 or newer from{' '}
<ExtLink href={LINKS.rustup}>rustup.rs</ExtLink>. Nix and Homebrew{' '}
<Code>--HEAD</Code> also build from source, but bring their own Rust.
</p>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
{INSTALLS.map((route) => (
<div
key={route.id}
id={route.id}
className="flex scroll-mt-20 flex-col gap-3.5 rounded-md border border-hairline bg-panel p-[18px] sm:p-[22px]"
>
<div className="flex items-center gap-2.5">
<OsIcon name={route.os} />
<h3 className="text-heading leading-[var(--text-heading--line-height)] font-medium">
{route.os}
</h3>
</div>
<span className="text-[11px] uppercase tracking-[var(--tracking-label)] text-secondary">
{route.best}
</span>
<Terminal lines={route.lines} wrap />
<p className="text-[12.5px] leading-5 text-secondary [&_code]:font-mono [&_code]:text-primary">
{route.alt}
</p>
</div>
))}
</div>
<InstallCards />
<LinuxRoutes />
<p className="text-[13px] text-muted">
Every route puts the same binary on your PATH. Run <Code>soulseek-rs</Code> in
a terminal and the TUI opens, in a script or a pipe it wants a subcommand
@@ -522,11 +442,13 @@ const Config = () => (
</Cols>
</Section>
)
// 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 = () => (
<Section band>
<SectionHead eyebrow="uninstall" title="Removing it completely.">
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{' '}
<Code>--log-file</Code> somewhere. No telemetry to opt out of.
</SectionHead>
<Cols start>
@@ -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 = () => (
<>
<PageHead eyebrow="install" title="Three ways in.">
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&rsquo;d rather. Or depend on the library and write your own client.
</PageHead>
<Steps />