feat(website): serve an install script that picks brew or the release binary

curl -fsSL https://re-invention.nl/soulseek-rs/install.sh | sh replaces the
brew one-liner as the front-page command: it works on Linux without Homebrew
by downloading the latest release archive for the platform, verifying its
sha256 and installing to /usr/local/bin or ~/.local/bin. Windows still gets
pointed at the msvc zip. scripts/test-install.sh covers target detection,
the brew fast path, checksum failure and real installs over curl and wget.
This commit is contained in:
Michel de Graaf
2026-07-29 16:22:05 +02:00
parent d2d1558708
commit 78a608c4c3
5 changed files with 272 additions and 9 deletions
+92
View File
@@ -0,0 +1,92 @@
#!/bin/sh
# Installs the latest soulseek-rs release.
#
# curl -fsSL https://re-invention.nl/soulseek-rs/install.sh | sh
#
# Uses Homebrew when it is on the PATH (macOS and Linux), otherwise downloads
# the prebuilt binary for this platform from GitHub releases, verifies its
# sha256, and puts it in /usr/local/bin or ~/.local/bin.
#
# SOULSEEK_RS_INSTALL_DIR overrides where the binary lands.
set -eu
REPO="michel/soulseek-rs"
say() { printf '%s\n' "$*"; }
die() {
printf 'install.sh: %s\n' "$*" >&2
exit 1
}
case "$(uname -s)" in
Darwin) os="apple-darwin" ;;
Linux) os="unknown-linux-musl" ;;
MINGW* | MSYS* | CYGWIN* | Windows_NT)
die "no install script for Windows: take the pc-windows-msvc zip from https://github.com/$REPO/releases/latest, or run 'cargo install soulseek-rs'" ;;
*) die "unsupported OS '$(uname -s)': see https://github.com/$REPO/releases/latest" ;;
esac
case "$(uname -m)" in
x86_64 | amd64) arch="x86_64" ;;
arm64 | aarch64) arch="aarch64" ;;
*) die "unsupported architecture '$(uname -m)': releases cover x86_64 and aarch64" ;;
esac
target="$arch-$os"
if command -v brew >/dev/null 2>&1; then
say "Homebrew found: installing michel/tap/soulseek-rs (brew also handles upgrades)"
exec brew install michel/tap/soulseek-rs
fi
if command -v curl >/dev/null 2>&1; then
fetch() { curl -fsSL "$1"; }
elif command -v wget >/dev/null 2>&1; then
fetch() { wget -qO- "$1"; }
else
die "need curl or wget"
fi
tag=$(fetch "https://api.github.com/repos/$REPO/releases/latest" |
sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n1)
[ -n "$tag" ] || die "could not resolve the latest release; downloads are at https://github.com/$REPO/releases/latest"
archive="soulseek-rs-$tag-$target.tar.gz"
base_url="https://github.com/$REPO/releases/download/$tag"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
say "Downloading $archive"
fetch "$base_url/$archive" >"$tmp/$archive"
fetch "$base_url/soulseek-rs-$tag-$target.sha256" >"$tmp/expected.sha256"
if command -v sha256sum >/dev/null 2>&1; then
got=$(sha256sum "$tmp/$archive")
else
got=$(shasum -a 256 "$tmp/$archive")
fi
got=${got%% *}
want=$(cut -d' ' -f1 <"$tmp/expected.sha256")
[ "$got" = "$want" ] || die "sha256 mismatch for $archive: expected $want, got $got"
tar -xzf "$tmp/$archive" -C "$tmp"
[ -f "$tmp/soulseek-rs" ] || die "archive $archive did not contain a soulseek-rs binary"
dir="${SOULSEEK_RS_INSTALL_DIR:-}"
if [ -z "$dir" ]; then
dir="/usr/local/bin"
{ [ -d "$dir" ] && [ -w "$dir" ]; } || dir="$HOME/.local/bin"
fi
mkdir -p "$dir"
# mv over a copy, not cp over the target: replacing a running binary in place
# fails with "text file busy" on Linux.
cp "$tmp/soulseek-rs" "$dir/.soulseek-rs.new"
chmod 755 "$dir/.soulseek-rs.new"
mv -f "$dir/.soulseek-rs.new" "$dir/soulseek-rs"
say "Installed $("$dir/soulseek-rs" --version) to $dir/soulseek-rs"
case ":$PATH:" in
*:"$dir":*) ;;
*) say "note: $dir is not on your PATH; add it with: export PATH=\"$dir:\$PATH\"" ;;
esac
+155
View File
@@ -0,0 +1,155 @@
#!/bin/sh
# Exercises public/install.sh: target detection, the brew fast path, checksum
# verification, and a real install into a throwaway prefix. Network tests hit
# GitHub; run from a machine that can reach it.
set -eu
here=$(cd "$(dirname "$0")" && pwd)
script="$here/../public/install.sh"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
failures=0
report() {
if [ "$1" = 0 ]; then
printf 'ok %s\n' "$2"
else
printf 'FAIL %s\n' "$2"
failures=$((failures + 1))
fi
}
contains() { case "$1" in *"$2"*) true ;; *) false ;; esac; }
# Stub uname reads STUB_OS / STUB_ARCH; stub curl logs every URL it is asked
# for, answers the release-API call with a canned tag, and refuses the rest so
# a detection test stops before downloading anything.
mkdir -p "$tmp/stub"
cat >"$tmp/stub/uname" <<'EOF'
#!/bin/sh
case "${1:-}" in
-m) echo "$STUB_ARCH" ;;
*) echo "$STUB_OS" ;;
esac
EOF
cat >"$tmp/stub/curl" <<EOF
#!/bin/sh
for a; do url=\$a; done
echo "\$url" >>"$tmp/curl.log"
case "\$url" in
*api.github.com*) echo '{"tag_name": "v12.0.0"}' ;;
*) exit 22 ;;
esac
EOF
chmod +x "$tmp/stub/uname" "$tmp/stub/curl"
base_path="/usr/bin:/bin"
resolved_target() {
: >"$tmp/curl.log"
STUB_OS="$1" STUB_ARCH="$2" PATH="$tmp/stub:$base_path" sh "$script" >/dev/null 2>&1 || true
grep -o 'soulseek-rs-v12\.0\.0-[a-z0-9_-]*\.tar\.gz' "$tmp/curl.log" | head -n1
}
for spec in \
"Darwin arm64 aarch64-apple-darwin" \
"Darwin x86_64 x86_64-apple-darwin" \
"Linux aarch64 aarch64-unknown-linux-musl" \
"Linux amd64 x86_64-unknown-linux-musl"; do
# spec is three space-separated fields, so the splitting is the point
# shellcheck disable=SC2086
set -- $spec
res=1
if [ "$(resolved_target "$1" "$2")" = "soulseek-rs-v12.0.0-$3.tar.gz" ]; then res=0; fi
report $res "$1/$2 resolves to $3"
done
out=$(STUB_OS="MINGW64_NT-10.0" STUB_ARCH=x86_64 PATH="$tmp/stub:$base_path" sh "$script" 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" != 0 ] && contains "$out" "pc-windows-msvc"; then res=0; fi
report $res "Windows refuses and points at the msvc zip"
out=$(STUB_OS=Linux STUB_ARCH=armv7l PATH="$tmp/stub:$base_path" sh "$script" 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" != 0 ] && contains "$out" "unsupported architecture"; then res=0; fi
report $res "armv7l refuses as unsupported"
# Brew fast path: a stub brew must be exec'd with the tap formula, and no URL
# may be fetched.
cat >"$tmp/stub/brew" <<EOF
#!/bin/sh
echo "\$*" >"$tmp/brew.log"
EOF
chmod +x "$tmp/stub/brew"
: >"$tmp/curl.log"
STUB_OS=Darwin STUB_ARCH=arm64 PATH="$tmp/stub:$base_path" sh "$script" >/dev/null 2>&1
res=1
if [ "$(cat "$tmp/brew.log")" = "install michel/tap/soulseek-rs" ] && [ ! -s "$tmp/curl.log" ]; then res=0; fi
report $res "brew on PATH wins and skips the download"
rm -f "$tmp/stub/brew"
# Offline happy path: stub curl serves a locally built archive whose sha256
# matches, and the installed fake binary must answer --version.
mkdir -p "$tmp/payload" "$tmp/bin"
printf '#!/bin/sh\necho "soulseek-rs 99.0.0-test"\n' >"$tmp/payload/soulseek-rs"
chmod +x "$tmp/payload/soulseek-rs"
tar -czf "$tmp/payload.tar.gz" -C "$tmp/payload" soulseek-rs
sum=$(shasum -a 256 "$tmp/payload.tar.gz" | cut -d' ' -f1)
cat >"$tmp/stub/curl" <<EOF
#!/bin/sh
for a; do url=\$a; done
echo "\$url" >>"$tmp/curl.log"
case "\$url" in
*api.github.com*) echo '{"tag_name": "v12.0.0"}' ;;
*.tar.gz) cat "$tmp/payload.tar.gz" ;;
*.sha256) echo "$sum *payload" ;;
*) exit 22 ;;
esac
EOF
out=$(STUB_OS=Darwin STUB_ARCH=arm64 SOULSEEK_RS_INSTALL_DIR="$tmp/bin" \
PATH="$tmp/stub:$base_path" sh "$script" 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" = 0 ] && [ -x "$tmp/bin/soulseek-rs" ] &&
contains "$out" "Installed soulseek-rs 99.0.0-test"; then res=0; fi
report $res "verified archive installs and reports the version"
res=1
if contains "$out" "not on your PATH"; then res=0; fi
report $res "install dir off the PATH gets the PATH note"
# Tampered archive: same stub but a wrong checksum must abort before install.
rm -f "$tmp/bin/soulseek-rs"
sed "s/$sum/0000000000000000000000000000000000000000000000000000000000000000/" \
"$tmp/stub/curl" >"$tmp/stub/curl.bad" && mv "$tmp/stub/curl.bad" "$tmp/stub/curl"
chmod +x "$tmp/stub/curl"
out=$(STUB_OS=Darwin STUB_ARCH=arm64 SOULSEEK_RS_INSTALL_DIR="$tmp/bin" \
PATH="$tmp/stub:$base_path" sh "$script" 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" != 0 ] && [ ! -e "$tmp/bin/soulseek-rs" ] &&
contains "$out" "sha256 mismatch"; then res=0; fi
report $res "sha256 mismatch aborts without installing"
# Real network installs: once over curl, once over wget (a symlink farm of
# /usr/bin and /bin minus curl and brew forces the wget code path).
out=$(SOULSEEK_RS_INSTALL_DIR="$tmp/real-curl" PATH="$base_path" sh "$script" 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" = 0 ] && "$tmp/real-curl/soulseek-rs" --version >/dev/null 2>&1; then res=0; fi
report $res "real release installs via curl and runs"
if wget=$(command -v wget); then
mkdir -p "$tmp/farm"
for d in /usr/bin /bin; do
for f in "$d"/*; do ln -sf "$f" "$tmp/farm/${f##*/}"; done
done
rm -f "$tmp/farm/curl" "$tmp/farm/brew"
ln -sf "$wget" "$tmp/farm/wget"
out=$(SOULSEEK_RS_INSTALL_DIR="$tmp/real-wget" PATH="$tmp/farm" sh "$script" 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" = 0 ] && "$tmp/real-wget/soulseek-rs" --version >/dev/null 2>&1; then res=0; fi
report $res "real release installs via wget and runs"
else
echo "skip real release installs via wget (no wget on this machine)"
fi
[ "$failures" = 0 ] || {
echo "$failures test(s) failed"
exit 1
}
echo "all tests passed"
+2
View File
@@ -18,3 +18,5 @@ export const LINKS = {
export const VERSION = __APP_VERSION__
export const SITE_URL = 'https://re-invention.nl/soulseek-rs/'
export const INSTALL_CMD = `curl -fsSL ${SITE_URL}install.sh | sh`
+6 -5
View File
@@ -10,12 +10,12 @@ import { Cols, Eyebrow, Prose, Section, SectionHead, Wrap } from '@/components/u
import { OsIcon, type OsName } from '@/components/ui/os-icon'
import { Callout } from '@/components/ui/panel'
import { Terminal } from '@/components/ui/terminal'
import { LINKS } from '@/lib/links'
import { INSTALL_CMD, LINKS } from '@/lib/links'
import { cn } from '@/lib/utils'
const PLATFORMS: readonly { name: OsName; note: string; anchor: string }[] = [
{ name: 'macOS', note: 'brew or cargo', anchor: 'macos' },
{ name: 'Linux', note: 'cargo install', anchor: 'linux' },
{ name: 'macOS', note: 'script or brew', anchor: 'macos' },
{ name: 'Linux', note: 'script or cargo', anchor: 'linux' },
{ name: 'Windows', note: 'cargo or .exe', anchor: 'windows' },
]
@@ -154,7 +154,7 @@ const Hero = () => (
<Button href="/docs">Read the docs</Button>
<Button href={LINKS.gh}>GitHub</Button>
</div>
<CommandPill text="brew install michel/tap/soulseek-rs" />
<CommandPill text={INSTALL_CMD} />
</div>
</div>
@@ -327,9 +327,10 @@ const GetStarted = () => (
<Terminal
label="quick start"
lines={[
{ t: 'cmd', text: 'brew install michel/tap/soulseek-rs' },
{ t: 'cmd', text: INSTALL_CMD },
{ t: 'cmd', text: 'soulseek-rs' },
]}
wrap
/>
</Cols>
</Section>
+17 -4
View File
@@ -6,7 +6,7 @@ import { Cols, PageHead, Prose, Section, SectionHead } from '@/components/ui/lay
import { OsIcon, type OsName } from '@/components/ui/os-icon'
import { Callout } from '@/components/ui/panel'
import { Terminal, type TermLine } from '@/components/ui/terminal'
import { LINKS } from '@/lib/links'
import { INSTALL_CMD, LINKS, SITE_URL } from '@/lib/links'
const LIB_SRC = `use soulseek_rs::Client;
use std::time::Duration;
@@ -266,7 +266,18 @@ const Steps = () => (
<div className="flex flex-col gap-9">
<Step n={1} title="Install the client">
<p className="text-secondary">
One command per platform. Cargo needs a Rust toolchain from{' '}
One command on macOS and Linux. The script installs through Homebrew when you
have it; otherwise it downloads the latest release binary for your platform,
checks its sha256, and puts it on your PATH.
</p>
<Terminal lines={[{ t: 'cmd', text: INSTALL_CMD }]} wrap />
<p className="text-[13px] text-muted">
No curl? <Code>{`wget -qO- ${SITE_URL}install.sh | sh`}</Code> does the same.
Piping to a shell asks for trust; read the script first at{' '}
<ExtLink href={`${SITE_URL}install.sh`}>{`${SITE_URL}install.sh`}</ExtLink>.
</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.
</p>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
@@ -520,6 +531,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: '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' },
{ t: 'cmd', text: 'brew untap michel/tap' },
{ t: 'cm', text: '# config and state: searches, downloads, rooms,' },
@@ -560,8 +573,8 @@ const Uninstall = () => (
export const Install = () => (
<>
<PageHead eyebrow="install" title="Three ways in.">
One command with Homebrew or cargo. Build from source if you&rsquo;d rather. Or depend
on the library and write your own client.
One command with the install script, Homebrew or cargo. Build from source if
you&rsquo;d rather. Or depend on the library and write your own client.
</PageHead>
<Steps />
<Reachable />