diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 005f468..4a5c9ee 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -24,6 +24,7 @@ jobs:
timeout-minutes: 5
outputs:
sha: ${{ steps.commit.outputs.sha }}
+ version: ${{ steps.commit.outputs.version }}
steps:
# workflow_dispatch has a branch selector, but a Nightly run always means
# develop. Resolve it once so every target builds the identical commit
@@ -36,13 +37,22 @@ jobs:
- name: Record source commit
id: commit
shell: bash
- run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
+ run: |
+ set -euo pipefail
+ sha=$(git rev-parse HEAD)
+ base=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2)
+ stamp=$(TZ=UTC git log -1 --format=%cd --date=format-local:%Y%m%d%H%M)
+ [[ "$base" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ && "$stamp" =~ ^[0-9]{12}$ ]]
+ echo "sha=$sha" >> "$GITHUB_OUTPUT"
+ echo "version=$base+git$stamp.${sha:0:7}" >> "$GITHUB_OUTPUT"
binaries:
name: ${{ matrix.target }}
needs: source
runs-on: ${{ matrix.os }}
timeout-minutes: 45
+ env:
+ SOULSEEK_RS_VERSION: ${{ needs.source.outputs.version }}
strategy:
fail-fast: false
matrix:
@@ -95,6 +105,12 @@ jobs:
dry-run: true
dry-run-intended: true
+ - name: Build .deb and .rpm
+ if: endsWith(matrix.target, '-musl')
+ env:
+ TARGET: ${{ matrix.target }}
+ run: sh scripts/package-linux.sh "target/$TARGET/release/soulseek-rs" "$SOULSEEK_RS_VERSION" "$TARGET" . soulseek-rs-nightly
+
- name: Store packaged binary
uses: actions/upload-artifact@v4
with:
@@ -102,6 +118,8 @@ jobs:
path: |
${{ steps.package.outputs.tar || steps.package.outputs.zip }}
${{ steps.package.outputs.sha256 }}
+ soulseek-rs-nightly-*.deb
+ soulseek-rs-nightly-*.rpm
if-no-files-found: error
retention-days: 1
@@ -137,6 +155,10 @@ jobs:
soulseek-rs-nightly-aarch64-apple-darwin.sha256
soulseek-rs-nightly-x86_64-pc-windows-msvc.sha256
soulseek-rs-nightly-aarch64-pc-windows-msvc.sha256
+ soulseek-rs-nightly-amd64.deb
+ soulseek-rs-nightly-arm64.deb
+ soulseek-rs-nightly-x86_64.rpm
+ soulseek-rs-nightly-aarch64.rpm
)
printf '%s\n' "${expected[@]}" | sort > "$RUNNER_TEMP/expected-assets"
find dist -maxdepth 1 -type f -exec basename {} \; | sort > "$RUNNER_TEMP/actual-assets"
@@ -219,9 +241,12 @@ jobs:
smoke-test:
name: Install published nightly
- needs: publish
+ needs: [source, publish]
runs-on: ubuntu-latest
timeout-minutes: 5
+ env:
+ VERSION: ${{ needs.source.outputs.version }}
+ DOWNLOAD: https://github.com/${{ github.repository }}/releases/download/nightly
steps:
- uses: actions/checkout@v4
with:
@@ -233,7 +258,7 @@ jobs:
SOULSEEK_RS_INSTALL_DIR: ${{ runner.temp }}/curl-install
run: |
sh website/public/install.sh --nightly
- "$SOULSEEK_RS_INSTALL_DIR/soulseek-rs" --version
+ "$SOULSEEK_RS_INSTALL_DIR/soulseek-rs" --version | grep -F "$VERSION"
- name: Install with wget and no curl on PATH
env:
@@ -246,4 +271,21 @@ jobs:
ln -s "$(command -v "$tool")" "$RUNNER_TEMP/wget-path/$tool"
done
PATH="$RUNNER_TEMP/wget-path" /bin/sh website/public/install.sh --nightly
- "$SOULSEEK_RS_INSTALL_DIR/soulseek-rs" --version
+ "$SOULSEEK_RS_INSTALL_DIR/soulseek-rs" --version | grep -F "$VERSION"
+
+ - name: Install the .deb
+ run: |
+ curl -fsSLO "$DOWNLOAD/soulseek-rs-nightly-amd64.deb"
+ sudo apt-get install -y ./soulseek-rs-nightly-amd64.deb
+ soulseek-rs --version | grep -F "$VERSION"
+ dpkg -L soulseek-rs | grep -Fx /usr/share/man/man1/soulseek-rs.1.gz
+ dpkg -L soulseek-rs | grep -Fx /usr/share/zsh/vendor-completions/_soulseek-rs
+
+ - name: Install the .rpm
+ run: |
+ docker run --rm fedora:latest sh -ec "
+ dnf install -y '$DOWNLOAD/soulseek-rs-nightly-x86_64.rpm'
+ soulseek-rs --version | grep -F '$VERSION'
+ rpm -ql soulseek-rs | grep -Fx /usr/share/man/man1/soulseek-rs.1.gz
+ rpm -ql soulseek-rs | grep -Fx /usr/share/zsh/site-functions/_soulseek-rs
+ "
diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml
new file mode 100644
index 0000000..bb1b304
--- /dev/null
+++ b/.github/workflows/packaging.yml
@@ -0,0 +1,87 @@
+name: Packaging
+
+on:
+ pull_request:
+ paths:
+ - 'flake.nix'
+ - 'flake.lock'
+ - 'Cargo.lock'
+ - 'Cargo.toml'
+ - 'packaging/**'
+ - 'scripts/package-linux.sh'
+ - 'soulseek-rs/src/cli.rs'
+ - 'soulseek-rs/src/run.rs'
+ - 'soulseek-rs/src/commands/**'
+ - '.github/workflows/packaging.yml'
+
+permissions:
+ contents: read
+
+concurrency:
+ group: packaging-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ CARGO_TERM_COLOR: always
+ VERSION: 0.0.0+git202601010000.0000000
+
+jobs:
+ packages:
+ name: .deb and .rpm
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: x86_64-unknown-linux-musl
+
+ - uses: Swatinem/rust-cache@v2
+
+ - name: Install musl toolchain
+ run: sudo apt-get update && sudo apt-get install -y musl-tools
+
+ - name: Build the packages
+ env:
+ SOULSEEK_RS_VERSION: ${{ env.VERSION }}
+ TARGET: x86_64-unknown-linux-musl
+ run: |
+ cargo build --locked --release --target "$TARGET" --package soulseek-rs
+ sh scripts/package-linux.sh "target/$TARGET/release/soulseek-rs" "$VERSION" "$TARGET" . soulseek-rs-nightly
+
+ - name: Install the .deb
+ run: |
+ sudo apt-get install -y ./soulseek-rs-nightly-amd64.deb
+ soulseek-rs --version | grep -F "$VERSION"
+ dpkg -L soulseek-rs | grep -Fx /usr/share/man/man1/soulseek-rs.1.gz
+ dpkg -L soulseek-rs | grep -Fx /usr/share/zsh/vendor-completions/_soulseek-rs
+
+ - name: Install the .rpm
+ run: |
+ docker run --rm -v "$PWD:/packages:ro" fedora:latest sh -ec "
+ dnf install -y /packages/soulseek-rs-nightly-x86_64.rpm
+ soulseek-rs --version | grep -F '$VERSION'
+ rpm -ql soulseek-rs | grep -Fx /usr/share/man/man1/soulseek-rs.1.gz
+ rpm -ql soulseek-rs | grep -Fx /usr/share/zsh/site-functions/_soulseek-rs
+ "
+
+ flake:
+ name: Nix flake
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ - uses: cachix/install-nix-action@v31
+
+ - name: Build and run
+ run: |
+ nix build --print-build-logs
+ ./result/bin/soulseek-rs --version | grep -E '\+git[0-9]{12}\.[0-9a-f]{7}$'
+ test -s result/share/man/man1/soulseek-rs.1.gz
+ test -s result/share/fish/vendor_completions.d/soulseek-rs.fish
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ac7f993..a7bbc65 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -200,6 +200,16 @@ jobs:
ref: refs/tags/${{ needs.release.outputs.tag }}
token: ${{ secrets.GITHUB_TOKEN }}
+ - name: Build and upload .deb and .rpm
+ if: endsWith(matrix.target, '-musl') && hashFiles('scripts/package-linux.sh') != ''
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ TAG: ${{ needs.release.outputs.tag }}
+ TARGET: ${{ matrix.target }}
+ run: |
+ sh scripts/package-linux.sh "target/$TARGET/release/soulseek-rs" "${TAG#v}" "$TARGET" dist
+ gh release upload "$TAG" dist/* --clobber
+
# The tap regenerates its formula from whatever `gh release view` calls latest,
# so it only needs a nudge once every archive and .sha256 is on the release —
# hence `needs: binaries` rather than firing alongside it. The tap also polls
diff --git a/Cargo.lock b/Cargo.lock
index e316a04..d57a443 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -476,9 +476,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.6.2"
+version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011"
+checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
dependencies = [
"clap_builder",
"clap_derive",
@@ -486,9 +486,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.6.2"
+version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
+checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
dependencies = [
"anstream",
"anstyle",
@@ -508,14 +508,14 @@ dependencies = [
[[package]]
name = "clap_derive"
-version = "4.6.1"
+version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.2",
]
[[package]]
@@ -524,6 +524,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+[[package]]
+name = "clap_mangen"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "211d617eaa4b735c96c9e0228fcbdb5120ef623f2b8cb67ffb84c3e02dbc28a4"
+dependencies = [
+ "clap",
+ "roff",
+]
+
[[package]]
name = "color-eyre"
version = "0.6.5"
@@ -2251,6 +2261,12 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+[[package]]
+name = "roff"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189"
+
[[package]]
name = "rustc-demangle"
version = "0.1.28"
@@ -2550,6 +2566,7 @@ dependencies = [
"chrono",
"clap",
"clap_complete",
+ "clap_mangen",
"color-eyre",
"directories",
"dotenv",
diff --git a/Cargo.toml b/Cargo.toml
index 310dc01..47a88b9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ resolver = "2"
version = "19.0.0"
authors = ["Michel de Graaf"]
edition = "2024"
+rust-version = "1.91"
license = "MIT"
repository = "https://github.com/michel/soulseek-rs"
homepage = "https://re-invention.nl/soulseek-rs/"
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/RELEASE.md b/RELEASE.md
index b6356d0..941db0f 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -28,6 +28,11 @@ Releases are automated by [release-plz](https://release-plz.dev). You do not run
| `soulseek-rs-vX.Y.Z-x86_64-pc-windows-msvc.zip` | |
| `soulseek-rs-vX.Y.Z-aarch64-pc-windows-msvc.zip` | |
+ - packages the two Linux binaries as `soulseek-rs-amd64.deb`, `soulseek-rs-arm64.deb`,
+ `soulseek-rs-x86_64.rpm` and `soulseek-rs-aarch64.rpm`, with the man page and the
+ bash, zsh and fish completions. The names carry no version, so
+ `releases/latest/download/soulseek-rs-amd64.deb` is a link that keeps working,
+
- rewrites `Formula/soulseek-rs.rb` in the `michel/homebrew-tap` repository so
`brew install michel/tap/soulseek-rs` picks up the new macOS and Linux archives,
- merges `master` back into `develop`, so the branch the work happens on carries the
@@ -44,9 +49,15 @@ conflict.
`.github/workflows/nightly.yml` builds the tip of `develop` at 02:17 UTC every
day. It resolves the commit once, builds the same six targets as a numbered
-release, verifies all twelve archives and checksum files as one set, then
-replaces the mutable `nightly` prerelease and tag. The published release is
-smoke-tested through both the curl and wget installer paths.
+release, packages the two Linux ones as `soulseek-rs-nightly-*.deb` and `.rpm`,
+verifies all sixteen files as one set, then replaces the mutable `nightly`
+prerelease and tag. Every nightly binary reports `X.Y.Z+git.`
+instead of the crate version. The published release is smoke-tested through the
+curl and wget installer paths, and by installing the `.deb` and the `.rpm`.
+
+Never enable GitHub's Immutable Releases on this repository: nightly replaces
+its assets and moves its tag, and a tag name burnt by that setting cannot be
+reused.
Use **Actions → Nightly → Run workflow** to force a build. The workflow always
checks out `develop`, regardless of the ref selected in the dispatch form. The
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..d9c0837
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1789785513,
+ "narHash": "sha256-B44WL6h0XoLjJ41bUPJk0X5SDinLCII//6EcBLXKiJ0=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "20b1ddd1aa5ace70c9468305030aa4f9ef79671b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..c954c07
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,51 @@
+{
+ description = "Soulseek client for the terminal, with a scriptable CLI and daemon";
+
+ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+
+ outputs =
+ { self, nixpkgs }:
+ let
+ inherit (nixpkgs) lib;
+ version = (lib.importTOML ./Cargo.toml).workspace.package.version;
+ systems = [
+ "x86_64-linux"
+ "aarch64-linux"
+ "aarch64-darwin"
+ ];
+ in
+ {
+ packages = lib.genAttrs systems (system: {
+ default = nixpkgs.legacyPackages.${system}.callPackage (
+ { rustPlatform, installShellFiles }:
+ rustPlatform.buildRustPackage {
+ pname = "soulseek-rs";
+ inherit version;
+ src = self;
+ cargoLock.lockFile = ./Cargo.lock;
+ cargoBuildFlags = [
+ "--package"
+ "soulseek-rs"
+ ];
+ doCheck = false;
+ nativeBuildInputs = [ installShellFiles ];
+ env.SOULSEEK_RS_VERSION = "${version}+git${lib.substring 0 12 self.lastModifiedDate}.${self.shortRev or "dirty"}";
+ postInstall = ''
+ installShellCompletion --cmd soulseek-rs \
+ --bash <($out/bin/soulseek-rs completions print bash) \
+ --zsh <($out/bin/soulseek-rs completions print zsh) \
+ --fish <($out/bin/soulseek-rs completions print fish)
+ $out/bin/soulseek-rs man > soulseek-rs.1
+ installManPage soulseek-rs.1
+ '';
+ meta = {
+ description = "Soulseek client for the terminal, with a scriptable CLI and daemon";
+ homepage = "https://re-invention.nl/soulseek-rs/";
+ license = lib.licenses.mit;
+ mainProgram = "soulseek-rs";
+ };
+ }
+ ) { };
+ });
+ };
+}
diff --git a/packaging/nfpm.yaml b/packaging/nfpm.yaml
new file mode 100644
index 0000000..1493db5
--- /dev/null
+++ b/packaging/nfpm.yaml
@@ -0,0 +1,55 @@
+name: soulseek-rs
+arch: ${PKG_ARCH}
+platform: linux
+version: ${PKG_VERSION}
+version_schema: none
+release: "1"
+section: net
+priority: optional
+maintainer: Michel de Graaf
+description: |
+ Soulseek client for the terminal
+ A TUI, a scriptable command line and a daemon for the Soulseek
+ peer-to-peer network.
+vendor: Michel de Graaf
+homepage: https://re-invention.nl/soulseek-rs/
+license: MIT
+suggests:
+ - gnome-keyring
+contents:
+ - src: target/nfpm/soulseek-rs
+ dst: /usr/bin/soulseek-rs
+ file_info:
+ mode: 0755
+ - src: target/nfpm/soulseek-rs.1.gz
+ dst: /usr/share/man/man1/soulseek-rs.1.gz
+ - src: target/nfpm/soulseek-rs.bash
+ dst: /usr/share/bash-completion/completions/soulseek-rs
+ - src: target/nfpm/soulseek-rs.fish
+ dst: /usr/share/fish/vendor_completions.d/soulseek-rs.fish
+ - src: target/nfpm/_soulseek-rs
+ dst: /usr/share/zsh/vendor-completions/_soulseek-rs
+ packager: deb
+ - src: target/nfpm/_soulseek-rs
+ dst: /usr/share/zsh/site-functions/_soulseek-rs
+ packager: rpm
+ - dst: /usr/share/doc/soulseek-rs
+ type: dir
+ packager: rpm
+ - dst: /usr/share/licenses/soulseek-rs
+ type: dir
+ packager: rpm
+ - src: README.md
+ dst: /usr/share/doc/soulseek-rs/README.md
+ packager: deb
+ - src: README.md
+ dst: /usr/share/doc/soulseek-rs/README.md
+ packager: rpm
+ type: doc
+ - src: LICENSE
+ dst: /usr/share/doc/soulseek-rs/copyright
+ packager: deb
+ - src: LICENSE
+ dst: /usr/share/licenses/soulseek-rs/LICENSE
+ packager: rpm
+ type: license
diff --git a/scripts/package-linux.sh b/scripts/package-linux.sh
new file mode 100755
index 0000000..8216098
--- /dev/null
+++ b/scripts/package-linux.sh
@@ -0,0 +1,54 @@
+#!/bin/sh
+set -eu
+umask 022
+
+[ $# -ge 4 ] || {
+ echo "usage: $0 []" >&2
+ exit 2
+}
+
+NFPM_VERSION=2.47.0
+
+binary=$1
+PKG_VERSION=$2
+target=$3
+out=$4
+package=${5:-soulseek-rs}
+
+case "$target" in
+ x86_64-*) PKG_ARCH=amd64 rpm_arch=x86_64 ;;
+ aarch64-*) PKG_ARCH=arm64 rpm_arch=aarch64 ;;
+ *) echo "no package architecture for $target" >&2; exit 1 ;;
+esac
+
+case "$(uname -s)-$(uname -m)" in
+ Linux-x86_64) nfpm_asset=Linux_x86_64 nfpm_sha256=0660ca602b2d2d2ae4781a06c692b3eeb9d437ffea05b831d76e41f4a3188783 ;;
+ Linux-aarch64) nfpm_asset=Linux_arm64 nfpm_sha256=1c0f5f2999b9a974bfb04fdb0cc3306096de530ac5dbb25d739cc5f5219c919c ;;
+ *) echo "no pinned nfpm build for this machine" >&2; exit 1 ;;
+esac
+
+root=$(cd "$(dirname "$0")/.." && pwd)
+work=$(mktemp -d)
+trap 'rm -rf "$work"' EXIT
+stage=$root/target/nfpm
+rm -rf "$stage"
+mkdir -p "$stage" "$out"
+out=$(cd "$out" && pwd)
+
+cp "$binary" "$stage/soulseek-rs"
+"$binary" completions print bash > "$stage/soulseek-rs.bash"
+"$binary" completions print zsh > "$stage/_soulseek-rs"
+"$binary" completions print fish > "$stage/soulseek-rs.fish"
+"$binary" man > "$stage/soulseek-rs.1"
+gzip -9n "$stage/soulseek-rs.1"
+
+archive=nfpm_${NFPM_VERSION}_$nfpm_asset.tar.gz
+curl -fsSL -o "$work/$archive" \
+ "https://github.com/goreleaser/nfpm/releases/download/v$NFPM_VERSION/$archive"
+echo "$nfpm_sha256 $work/$archive" | sha256sum -c - >/dev/null
+tar -xzf "$work/$archive" -C "$work" nfpm
+
+export PKG_ARCH PKG_VERSION
+cd "$root"
+"$work/nfpm" package --config packaging/nfpm.yaml --packager deb --target "$out/$package-$PKG_ARCH.deb"
+"$work/nfpm" package --config packaging/nfpm.yaml --packager rpm --target "$out/$package-$rpm_arch.rpm"
diff --git a/soulseek-rs-lib/Cargo.toml b/soulseek-rs-lib/Cargo.toml
index ebc7d14..659c074 100644
--- a/soulseek-rs-lib/Cargo.toml
+++ b/soulseek-rs-lib/Cargo.toml
@@ -3,6 +3,7 @@ name = "soulseek-rs-lib"
version.workspace = true
authors.workspace = true
edition.workspace = true
+rust-version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
diff --git a/soulseek-rs/Cargo.toml b/soulseek-rs/Cargo.toml
index 1465749..f40af90 100644
--- a/soulseek-rs/Cargo.toml
+++ b/soulseek-rs/Cargo.toml
@@ -3,6 +3,7 @@ name = "soulseek-rs"
version.workspace = true
authors.workspace = true
edition.workspace = true
+rust-version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -29,6 +30,7 @@ soulseek-rs-lib = { version = "19.0.0", path = "../soulseek-rs-lib" }
clap = { version = "4.6.2", features = ["derive", "color", "wrap_help", "env"] }
clap_complete = "4.6.7"
+clap_mangen = "0.3.3"
ratatui = "0.30.2"
unicode-width = "0.2"
color-eyre = "0.6.5"
diff --git a/soulseek-rs/src/SKILL.md b/soulseek-rs/src/SKILL.md
index 2dc21fc..387db6d 100644
--- a/soulseek-rs/src/SKILL.md
+++ b/soulseek-rs/src/SKILL.md
@@ -236,6 +236,9 @@ this skill file into the local agent directories, or remove it: `agent`,
completion script where bash, zsh, or fish looks for it, or take it back
again: `shell`, `path`, `action`. For a human at a prompt, not for you.
+**`completions print `** and **`man`** — the completion script or the
+man page on stdout, raw rather than records. For whoever builds a package.
+
## Idioms
```bash
diff --git a/soulseek-rs/src/cli.rs b/soulseek-rs/src/cli.rs
index 8c5efd0..37b3270 100644
--- a/soulseek-rs/src/cli.rs
+++ b/soulseek-rs/src/cli.rs
@@ -34,11 +34,16 @@ Exit codes:
0 success 2 usage/config 3 connect or login 4 no results
5 timed out 6 transfer failed 7 session lost 1 unexpected error";
+pub const VERSION: &str = match option_env!("SOULSEEK_RS_VERSION") {
+ Some(version) => version,
+ None => env!("CARGO_PKG_VERSION"),
+};
+
#[derive(Parser, Debug, Default)]
#[command(
name = "soulseek-rs",
author,
- version,
+ version = VERSION,
about = "Soulseek client in Rust 🦀",
long_about = "Soulseek client in Rust.\n\nRun without a subcommand for the \
interactive TUI, or use a subcommand for a scriptable \
@@ -244,6 +249,9 @@ pub enum Commands {
/// Tab completion for bash, zsh, and fish
#[command(subcommand)]
Completions(CompletionsCommand),
+
+ /// Print the man page to stdout, for packaging
+ Man,
}
#[derive(Args, Debug, Default)]
@@ -310,6 +318,12 @@ pub enum CompletionsCommand {
/// Remove the completion script again
Uninstall(CompletionsArgs),
+
+ /// Print the completion script to stdout, for packaging
+ Print {
+ #[arg(value_enum)]
+ shell: Shell,
+ },
}
#[derive(Args, Debug)]
diff --git a/soulseek-rs/src/commands/completions.rs b/soulseek-rs/src/commands/completions.rs
index d6ca9ef..cd5582c 100644
--- a/soulseek-rs/src/commands/completions.rs
+++ b/soulseek-rs/src/commands/completions.rs
@@ -8,10 +8,10 @@
use super::{unwritable, xdg};
use crate::cli::{Cli, CompletionsArgs, CompletionsCommand, Shell};
-use crate::output::{CliError, CliResult, CompletionRecord, Out};
+use crate::output::{CliError, CliResult, CompletionRecord, Exit, Out};
use clap::CommandFactory;
use directories::UserDirs;
-use std::io::Write;
+use std::io::{ErrorKind, Write};
use std::path::{Path, PathBuf};
/// The command being completed, and the file name the scripts are written as.
@@ -65,6 +65,27 @@ pub fn run(out: &Out, command: &CompletionsCommand) -> CliResult {
match command {
CompletionsCommand::Install(args) => install(out, &targets(args)?),
CompletionsCommand::Uninstall(args) => uninstall(out, &targets(args)?),
+ CompletionsCommand::Print { shell } => print(&script(*shell)),
+ }
+}
+
+pub fn man() -> CliResult {
+ let mut page = Vec::new();
+ clap_mangen::Man::new(Cli::command().about("Soulseek client in Rust"))
+ .render(&mut page)
+ .expect("a Vec accepts every write");
+ print(&page)
+}
+
+fn print(bytes: &[u8]) -> CliResult {
+ match std::io::stdout().write_all(bytes) {
+ Err(error) if error.kind() != ErrorKind::BrokenPipe => {
+ Err(CliError::new(
+ Exit::Failure,
+ format!("cannot write stdout: {error}"),
+ ))
+ }
+ _ => Ok(()),
}
}
diff --git a/soulseek-rs/src/commands/mod.rs b/soulseek-rs/src/commands/mod.rs
index c5a009e..65192e9 100644
--- a/soulseek-rs/src/commands/mod.rs
+++ b/soulseek-rs/src/commands/mod.rs
@@ -330,7 +330,8 @@ fn dispatch(ctx: &Ctx, command: Commands) -> CliResult {
| Commands::Config(_)
| Commands::Shares(_)
| Commands::Skills(_)
- | Commands::Completions(_) => {
+ | Commands::Completions(_)
+ | Commands::Man => {
Err(CliError::usage("handled without a session; see main::run"))
}
}
diff --git a/soulseek-rs/src/daemon/server/mod.rs b/soulseek-rs/src/daemon/server/mod.rs
index ca7fe3c..201a6d0 100644
--- a/soulseek-rs/src/daemon/server/mod.rs
+++ b/soulseek-rs/src/daemon/server/mod.rs
@@ -522,7 +522,7 @@ impl Daemon {
fn identify(&self) -> AuthResult {
AuthResult {
protocol: PROTOCOL_VERSION,
- daemon_version: env!("CARGO_PKG_VERSION").to_string(),
+ daemon_version: crate::cli::VERSION.to_string(),
username: self.session.username(),
}
}
@@ -532,7 +532,7 @@ impl Daemon {
DaemonStatus {
username: self.session.username(),
server: self.server.clone(),
- daemon_version: env!("CARGO_PKG_VERSION").to_string(),
+ daemon_version: crate::cli::VERSION.to_string(),
protocol: PROTOCOL_VERSION,
listen_port: self.session.listen_port(),
shared_folders: folders,
diff --git a/soulseek-rs/src/run.rs b/soulseek-rs/src/run.rs
index eeecf45..0d653b3 100644
--- a/soulseek-rs/src/run.rs
+++ b/soulseek-rs/src/run.rs
@@ -35,6 +35,14 @@ pub fn run(mut cli: Cli, out: &Out) -> CliResult {
}
}
+ match &cli.command {
+ Some(Commands::Completions(command)) => {
+ return crate::commands::completions::run(out, command);
+ }
+ Some(Commands::Man) => return crate::commands::completions::man(),
+ _ => {}
+ }
+
let config_path = config_path(&cli);
let file_config = match &config_path {
Some(path) => crate::persist::config::FileConfig::load(path)
@@ -84,9 +92,6 @@ pub fn run(mut cli: Cli, out: &Out) -> CliResult {
resolved.username.as_deref(),
);
}
- Commands::Completions(ref command) => {
- return crate::commands::completions::run(out, command);
- }
Commands::Config(ConfigCommand::Path) => {
return crate::commands::settings::config_path(out, &store);
}
diff --git a/soulseek-rs/src/ui/downloads.rs b/soulseek-rs/src/ui/downloads.rs
index f79f718..24eec71 100644
--- a/soulseek-rs/src/ui/downloads.rs
+++ b/soulseek-rs/src/ui/downloads.rs
@@ -1,3 +1,4 @@
+use crate::cli::VERSION;
use crate::models::DownloadEntry;
use crate::ui::{
BYTES_PER_MB, accent_style, dimmed_style, error_style,
@@ -14,8 +15,6 @@ use ratatui::{
use soulseek_rs::DownloadStatus;
use std::fmt::Write as _;
-const VERSION: &str = env!("CARGO_PKG_VERSION");
-
/// Bytes transferred, bytes expected and combined speed across the downloads
/// that are actually running — or `None` when none are, so the caller can hide
/// the progress readout entirely.
diff --git a/soulseek-rs/src/ui/panes/results_pane.rs b/soulseek-rs/src/ui/panes/results_pane.rs
index 1b71f59..e111655 100644
--- a/soulseek-rs/src/ui/panes/results_pane.rs
+++ b/soulseek-rs/src/ui/panes/results_pane.rs
@@ -1,4 +1,5 @@
use super::name_scroll::{column_width, end_offset, scroll_text};
+use crate::cli::VERSION;
use crate::models::FileDisplayData;
use crate::ui::{
BYTES_PER_MB, HIGHLIGHT_SYMBOL, body_style, dimmed_style, format_bytes,
@@ -13,8 +14,6 @@ use ratatui::{
};
use std::collections::HashSet;
-const VERSION: &str = env!("CARGO_PKG_VERSION");
-
// The file name and its folder share whatever width the fixed columns leave,
// the name getting the larger part: it is what a search is after, and a
// folder's tail is a scroll away.
diff --git a/soulseek-rs/tests/cli_e2e.rs b/soulseek-rs/tests/cli_e2e.rs
index efea975..6a44531 100644
--- a/soulseek-rs/tests/cli_e2e.rs
+++ b/soulseek-rs/tests/cli_e2e.rs
@@ -412,6 +412,28 @@ fn installing_completions_is_repeatable_and_reversible() {
assert_eq!(action(&again), "absent");
}
+#[test]
+fn a_package_build_reads_completions_and_the_man_page_from_stdout() {
+ let dir = Scratch::new("package-build");
+ let config = dir.path().join("config.toml");
+ std::fs::write(&config, "not = [toml").expect("a config file");
+ let broken = config.display().to_string();
+
+ for (shell, marker) in [
+ ("bash", "complete -F"),
+ ("zsh", "#compdef soulseek-rs"),
+ ("fish", "complete -c soulseek-rs"),
+ ] {
+ let output = run(&["--config", &broken, "completions", "print", shell]);
+ assert_eq!(code(&output), EXIT_OK, "stderr: {}", stderr(&output));
+ assert!(stdout(&output).contains(marker), "{shell} script");
+ }
+
+ let man = run(&["--config", &broken, "man"]);
+ assert_eq!(code(&man), EXIT_OK, "stderr: {}", stderr(&man));
+ assert!(stdout(&man).contains(".TH soulseek-rs 1"));
+}
+
// --- wishlist, the half that only touches the config file ------------------
#[test]
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.