Merge pull request #92 from michel/nightly

feat(ci): publish installable nightly builds
This commit is contained in:
Michel de Graaf
2026-09-08 14:07:35 +02:00
committed by GitHub
9 changed files with 762 additions and 142 deletions
+231
View File
@@ -0,0 +1,231 @@
name: Nightly
on:
schedule:
- cron: '17 2 * * *'
workflow_dispatch:
permissions:
contents: read
# A manual run queued behind the scheduled run must still execute: it is the
# recovery path when a runner or GitHub Releases had a transient failure.
concurrency:
group: nightly
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
jobs:
source:
name: Resolve develop
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
sha: ${{ steps.commit.outputs.sha }}
steps:
# workflow_dispatch has a branch selector, but a Nightly run always means
# develop. Resolve it once so every target builds the identical commit
# even if develop moves while the matrix is starting.
- uses: actions/checkout@v4
with:
ref: develop
persist-credentials: false
- name: Record source commit
id: commit
shell: bash
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
binaries:
name: ${{ matrix.target }}
needs: source
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
- target: aarch64-unknown-linux-musl
os: ubuntu-24.04-arm
- target: x86_64-apple-darwin
os: macos-latest
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-pc-windows-msvc
os: windows-latest
- target: aarch64-pc-windows-msvc
os: windows-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.source.outputs.sha }}
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: nightly-${{ matrix.target }}
- name: Install musl toolchain
if: endsWith(matrix.target, '-musl')
run: sudo apt-get update && sudo apt-get install -y musl-tools
# Dry-run produces the same archives and checksums as the stable release
# workflow without publishing them one matrix job at a time. The publish
# job exposes the complete, verified set together.
- name: Build archive and checksum
id: package
uses: taiki-e/upload-rust-binary-action@v1
with:
bin: soulseek-rs
target: ${{ matrix.target }}
build-tool: cargo
locked: true
archive: soulseek-rs-nightly-$target
include: README.md,LICENSE
checksum: sha256
ref: refs/tags/nightly
dry-run: true
dry-run-intended: true
- name: Store packaged binary
uses: actions/upload-artifact@v4
with:
name: nightly-${{ matrix.target }}
path: |
${{ steps.package.outputs.tar || steps.package.outputs.zip }}
${{ steps.package.outputs.sha256 }}
if-no-files-found: error
retention-days: 1
publish:
name: Publish prerelease
needs: [source, binaries]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Collect packaged binaries
uses: actions/download-artifact@v4
with:
pattern: nightly-*
path: dist
merge-multiple: true
- name: Verify the complete asset set
shell: bash
run: |
set -euo pipefail
expected=(
soulseek-rs-nightly-x86_64-unknown-linux-musl.tar.gz
soulseek-rs-nightly-aarch64-unknown-linux-musl.tar.gz
soulseek-rs-nightly-x86_64-apple-darwin.tar.gz
soulseek-rs-nightly-aarch64-apple-darwin.tar.gz
soulseek-rs-nightly-x86_64-pc-windows-msvc.zip
soulseek-rs-nightly-aarch64-pc-windows-msvc.zip
soulseek-rs-nightly-x86_64-unknown-linux-musl.sha256
soulseek-rs-nightly-aarch64-unknown-linux-musl.sha256
soulseek-rs-nightly-x86_64-apple-darwin.sha256
soulseek-rs-nightly-aarch64-apple-darwin.sha256
soulseek-rs-nightly-x86_64-pc-windows-msvc.sha256
soulseek-rs-nightly-aarch64-pc-windows-msvc.sha256
)
printf '%s\n' "${expected[@]}" | sort > "$RUNNER_TEMP/expected-assets"
find dist -maxdepth 1 -type f -exec basename {} \; | sort > "$RUNNER_TEMP/actual-assets"
diff -u "$RUNNER_TEMP/expected-assets" "$RUNNER_TEMP/actual-assets"
(cd dist && sha256sum --check ./*.sha256)
- name: Publish the nightly release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
SOURCE_SHA: ${{ needs.source.outputs.sha }}
shell: bash
run: |
set -euo pipefail
notes="$RUNNER_TEMP/nightly-notes.md"
short_sha=${SOURCE_SHA:0:12}
{
printf 'Automated development build from the develop branch.\n\n'
printf 'Source: [%s](https://github.com/%s/commit/%s)\n\n' "$short_sha" "$GITHUB_REPOSITORY" "$SOURCE_SHA"
printf 'This prerelease and its nightly tag are replaced by each successful scheduled or manual run. It may be less stable than a numbered release.\n'
} > "$notes"
release_exists=false
if gh release view nightly >/dev/null 2>&1; then
release_exists=true
# Hide the old set while assets and the mutable tag are replaced;
# installers either see a complete release or fail before changing
# their existing binary.
gh release edit nightly --draft --prerelease --latest=false
fi
current_tag=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/nightly" --jq .object.sha 2>/dev/null || true)
if [ -n "$current_tag" ]; then
gh api --silent --method PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/nightly" \
-f sha="$SOURCE_SHA" -F force=true
else
gh api --silent --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
-f ref=refs/tags/nightly -f sha="$SOURCE_SHA"
fi
if [ "$release_exists" = false ]; then
gh release create nightly --verify-tag --draft --prerelease --latest=false \
--title 'Nightly (develop)' --notes-file "$notes"
else
gh release edit nightly --draft --prerelease --latest=false \
--target "$SOURCE_SHA" --title 'Nightly (develop)' --notes-file "$notes"
fi
gh release view nightly --json assets --jq '.assets[].name' > "$RUNNER_TEMP/old-assets"
while IFS= read -r asset; do
[ -z "$asset" ] || gh release delete-asset nightly "$asset" --yes
done < "$RUNNER_TEMP/old-assets"
gh release upload nightly dist/*
gh release view nightly --json assets --jq '.assets[].name' | sort > "$RUNNER_TEMP/published-assets"
diff -u "$RUNNER_TEMP/expected-assets" "$RUNNER_TEMP/published-assets"
empty_assets=$(gh release view nightly --json assets --jq '[.assets[] | select(.size == 0)] | length')
[ "$empty_assets" -eq 0 ]
gh release edit nightly --draft=false --prerelease --latest=false \
--target "$SOURCE_SHA" --title 'Nightly (develop)' --notes-file "$notes"
smoke-test:
name: Install published nightly
needs: publish
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
ref: nightly
persist-credentials: false
- name: Install with curl
env:
SOULSEEK_RS_INSTALL_DIR: ${{ runner.temp }}/curl-install
run: |
sh website/public/install.sh --nightly
"$SOULSEEK_RS_INSTALL_DIR/soulseek-rs" --version
- name: Install with wget and no curl on PATH
env:
SOULSEEK_RS_INSTALL_DIR: ${{ runner.temp }}/wget-install
shell: bash
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/wget-path"
for tool in basename cat chmod cp gzip mkdir mktemp mv rm sha256sum tar uname wget; do
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
+3
View File
@@ -30,6 +30,9 @@ jobs:
- name: Install
run: bun install --frozen-lockfile
- name: Test installer
run: bun run test:install
- name: Typecheck
run: bun run typecheck
+21 -1
View File
@@ -76,7 +76,27 @@ project is identifiable on the network.
## Installation
Homebrew ships a prebuilt binary, so no Rust toolchain:
The installer picks the right macOS or Linux binary, verifies its sha256, and
installs the latest stable release (through Homebrew when it is available):
```bash
curl -fsSL https://re-invention.nl/soulseek-rs/install.sh | sh
```
To install the current successful build from `develop` instead, pass the
installer's `--nightly` option through to `sh`:
```bash
curl -fsSL https://re-invention.nl/soulseek-rs/install.sh | sh -s -- --nightly
```
Nightly is a mutable prerelease rebuilt every day and whenever the **Nightly**
workflow is run by hand. It bypasses Homebrew, and may be less stable than a
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:
```bash
brew install michel/tap/soulseek-rs # macOS and Linux
+19
View File
@@ -33,6 +33,25 @@ Releases are automated by [release-plz](https://release-plz.dev). You do not run
Both crates always share one version, one tag, and one changelog — see `release-plz.toml`.
## Nightly builds
`.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.
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
equivalent command is:
```bash
gh workflow run nightly.yml --ref develop
```
Nightly does not publish crates or update Homebrew. If a target fails, the
previous nightly release stays available; use a manual run to retry.
## One-time repository setup
- **Settings → Actions → General → Workflow permissions**: enable *Allow GitHub Actions to
+1
View File
@@ -9,6 +9,7 @@
"build:ssr": "vite build --ssr src/entry-server.tsx --outDir dist-ssr",
"prerender": "bun scripts/prerender.ts",
"preview": "vite preview",
"test:install": "sh scripts/test-install.sh",
"typecheck": "tsc -b",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
+117 -33
View File
@@ -1,11 +1,15 @@
#!/bin/sh
# Installs the latest soulseek-rs release.
# Installs soulseek-rs from GitHub releases.
#
# Latest stable 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.
# Latest successful build from develop:
# curl -fsSL https://re-invention.nl/soulseek-rs/install.sh | sh -s -- --nightly
#
# Stable installs use Homebrew when it is on PATH (macOS and Linux). Nightly
# installs always download the prebuilt binary for this platform. Direct
# downloads are sha256-verified and replace an existing binary atomically.
#
# SOULSEEK_RS_INSTALL_DIR overrides where the binary lands.
set -eu
@@ -17,23 +21,66 @@ die() {
printf 'install.sh: %s\n' "$*" >&2
exit 1
}
usage() {
cat <<'EOF'
Usage: install.sh [--nightly]
case "$(uname -s)" in
Install the latest stable soulseek-rs release. Options:
--nightly Install the latest successful build from the develop branch.
-h, --help Show this help.
When piping the script, pass the option after `sh -s --`:
curl -fsSL https://re-invention.nl/soulseek-rs/install.sh | sh -s -- --nightly
SOULSEEK_RS_INSTALL_DIR overrides the destination directory.
EOF
}
channel="stable"
while [ "$#" -gt 0 ]; do
case "$1" in
--nightly) channel="nightly" ;;
-h | --help)
usage
exit 0
;;
--)
shift
[ "$#" -eq 0 ] || die "unexpected argument '$1' (try --help)"
break
;;
*) die "unknown option '$1' (try --help)" ;;
esac
shift
done
if [ "$channel" = "nightly" ]; then
tag="nightly"
release_page="https://github.com/$REPO/releases/tag/nightly"
else
tag=""
release_page="https://github.com/$REPO/releases/latest"
fi
system=$(uname -s)
case "$system" 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" ;;
die "no install script for Windows: take the pc-windows-msvc zip from $release_page, or run 'cargo install soulseek-rs'" ;;
*) die "unsupported OS '$system': see $release_page" ;;
esac
case "$(uname -m)" in
machine=$(uname -m)
case "$machine" in
x86_64 | amd64) arch="x86_64" ;;
arm64 | aarch64) arch="aarch64" ;;
*) die "unsupported architecture '$(uname -m)': releases cover x86_64 and aarch64" ;;
*) die "unsupported architecture '$machine': builds cover x86_64 and aarch64" ;;
esac
target="$arch-$os"
if command -v brew >/dev/null 2>&1; then
if [ "$channel" = "stable" ] && [ -z "${SOULSEEK_RS_INSTALL_DIR:-}" ] &&
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
@@ -46,47 +93,84 @@ 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"
command -v tar >/dev/null 2>&1 || die "need tar to unpack the download"
if command -v sha256sum >/dev/null 2>&1; then
sha256() { sha256sum "$1"; }
elif command -v shasum >/dev/null 2>&1; then
sha256() { shasum -a 256 "$1"; }
else
die "need sha256sum or shasum to verify the download"
fi
if [ "$channel" = "stable" ]; then
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 $release_page"
case "$tag" in
v*.*.*) ;;
*) die "latest release returned an unexpected tag '$tag'" ;;
esac
case "$tag" in
*[!0-9A-Za-z._+-]*) die "latest release returned an unsafe tag '$tag'" ;;
esac
fi
archive="soulseek-rs-$tag-$target.tar.gz"
base_url="https://github.com/$REPO/releases/download/$tag"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
tmp=""
staged=""
cleanup() {
[ -z "$staged" ] || rm -f "$staged" >/dev/null 2>&1 || :
[ -z "$tmp" ] || rm -rf "$tmp" >/dev/null 2>&1 || :
}
trap cleanup EXIT HUP INT TERM
tmp=$(mktemp -d) || die "could not create a temporary directory"
say "Downloading $archive"
fetch "$base_url/$archive" >"$tmp/$archive"
fetch "$base_url/soulseek-rs-$tag-$target.sha256" >"$tmp/expected.sha256"
fetch "$base_url/$archive" >"$tmp/$archive" || die "could not download $archive from $release_page"
fetch "$base_url/soulseek-rs-$tag-$target.sha256" >"$tmp/expected.sha256" ||
die "could not download the sha256 checksum from $release_page"
if command -v sha256sum >/dev/null 2>&1; then
got=$(sha256sum "$tmp/$archive")
else
got=$(shasum -a 256 "$tmp/$archive")
fi
got=$(sha256 "$tmp/$archive") || die "could not calculate the sha256 for $archive"
got=${got%% *}
want=$(cut -d' ' -f1 <"$tmp/expected.sha256")
want=""
IFS=' ' read -r want _ <"$tmp/expected.sha256" || :
[ -n "$want" ] || die "the sha256 checksum from $release_page was empty"
[ "$got" = "$want" ] || die "sha256 mismatch for $archive: expected $want, got $got"
tar -xzf "$tmp/$archive" -C "$tmp"
tar -xzf "$tmp/$archive" -C "$tmp" || die "could not unpack $archive"
[ -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"
if ! { [ -d "$dir" ] && [ -w "$dir" ]; }; then
[ -n "${HOME:-}" ] ||
die "HOME is not set and /usr/local/bin is not writable; set SOULSEEK_RS_INSTALL_DIR"
dir="$HOME/.local/bin"
fi
fi
mkdir -p "$dir"
mkdir -p "$dir" || die "could not create install directory '$dir'"
[ -d "$dir" ] || die "install destination '$dir' is not a directory"
[ ! -d "$dir/soulseek-rs" ] || die "install destination '$dir/soulseek-rs' is a directory"
# 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"
staged=$(mktemp "$dir/.soulseek-rs.XXXXXX") ||
die "could not create a temporary file in '$dir'"
cp "$tmp/soulseek-rs" "$staged" || die "could not stage the downloaded binary in '$dir'"
chmod 755 "$staged" || die "could not make the downloaded binary executable"
version=$("$staged" --version) ||
die "the downloaded binary could not run on this machine; the existing install was not changed"
case "$version" in
soulseek-rs*) ;;
*) die "the downloaded binary returned unexpected version output '$version'; the existing install was not changed" ;;
esac
say "Installed $("$dir/soulseek-rs" --version) to $dir/soulseek-rs"
case ":$PATH:" in
mv -f "$staged" "$dir/soulseek-rs" || die "could not replace '$dir/soulseek-rs'"
staged=""
say "Installed $version ($channel) to $dir/soulseek-rs"
case ":${PATH:-}:" in
*:"$dir":*) ;;
*) say "note: $dir is not on your PATH; add it with: export PATH=\"$dir:\$PATH\"" ;;
esac
+347 -103
View File
@@ -1,17 +1,17 @@
#!/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.
# Hermetic coverage for public/install.sh. Set INSTALL_TEST_NETWORK=stable to
# add a live stable-release install, or =all after a nightly release exists to
# exercise both public channels.
set -eu
here=$(cd "$(dirname "$0")" && pwd)
script="$here/../public/install.sh"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
failures=0
report() {
if [ "$1" = 0 ]; then
if [ "$1" -eq 0 ]; then
printf 'ok %s\n' "$2"
else
printf 'FAIL %s\n' "$2"
@@ -20,33 +20,133 @@ report() {
}
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.
if command -v sha256sum >/dev/null 2>&1; then
fixture_sha256() { sha256sum "$1"; }
else
fixture_sha256() { shasum -a 256 "$1"; }
fi
mkdir -p "$tmp/fixtures/good" "$tmp/fixtures/missing" "$tmp/fixtures/bad-version" \
"$tmp/fixtures/cannot-run"
printf '#!/bin/sh\nprintf "soulseek-rs 99.0.0-test\\n"\n' >"$tmp/fixtures/good/soulseek-rs"
printf 'no binary here\n' >"$tmp/fixtures/missing/README.md"
printf '#!/bin/sh\nprintf "not-the-client 99.0.0-test\\n"\n' >"$tmp/fixtures/bad-version/soulseek-rs"
printf '#!/bin/sh\nexit 7\n' >"$tmp/fixtures/cannot-run/soulseek-rs"
chmod +x "$tmp/fixtures/good/soulseek-rs" "$tmp/fixtures/bad-version/soulseek-rs" \
"$tmp/fixtures/cannot-run/soulseek-rs"
tar -czf "$tmp/fixtures/good.tar.gz" -C "$tmp/fixtures/good" soulseek-rs
tar -czf "$tmp/fixtures/missing.tar.gz" -C "$tmp/fixtures/missing" README.md
tar -czf "$tmp/fixtures/bad-version.tar.gz" -C "$tmp/fixtures/bad-version" soulseek-rs
tar -czf "$tmp/fixtures/cannot-run.tar.gz" -C "$tmp/fixtures/cannot-run" soulseek-rs
printf 'this is not a tar archive\n' >"$tmp/fixtures/corrupt.tar.gz"
fixture_sum=$(fixture_sha256 "$tmp/fixtures/good.tar.gz")
fixture_sum=${fixture_sum%% *}
printf '%s fixture.tar.gz\n' "$fixture_sum" >"$tmp/fixtures/good.sha256"
missing_sum=$(fixture_sha256 "$tmp/fixtures/missing.tar.gz")
missing_sum=${missing_sum%% *}
printf '%s fixture.tar.gz\n' "$missing_sum" >"$tmp/fixtures/missing.sha256"
bad_version_sum=$(fixture_sha256 "$tmp/fixtures/bad-version.tar.gz")
bad_version_sum=${bad_version_sum%% *}
printf '%s fixture.tar.gz\n' "$bad_version_sum" >"$tmp/fixtures/bad-version.sha256"
cannot_run_sum=$(fixture_sha256 "$tmp/fixtures/cannot-run.tar.gz")
cannot_run_sum=${cannot_run_sum%% *}
printf '%s fixture.tar.gz\n' "$cannot_run_sum" >"$tmp/fixtures/cannot-run.sha256"
corrupt_sum=$(fixture_sha256 "$tmp/fixtures/corrupt.tar.gz")
corrupt_sum=${corrupt_sum%% *}
printf '%s fixture.tar.gz\n' "$corrupt_sum" >"$tmp/fixtures/corrupt.sha256"
# Every fetch is local. The same stub accepts curl's and wget's argument forms,
# records the final URL, and can inject each failure mode the installer handles.
mkdir -p "$tmp/stub"
cat >"$tmp/stub/uname" <<'EOF'
#!/bin/sh
[ -z "${UNAME_LOG:-}" ] || printf '%s\n' "${1:-system}" >>"$UNAME_LOG"
case "${1:-}" in
-m) echo "$STUB_ARCH" ;;
*) echo "$STUB_OS" ;;
-m) printf '%s\n' "$STUB_ARCH" ;;
*) printf '%s\n' "$STUB_OS" ;;
esac
EOF
cat >"$tmp/stub/curl" <<EOF
cat >"$tmp/stub/fetch" <<'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"}' ;;
url=""
for argument do url=$argument; done
[ -z "${FETCH_LOG:-}" ] || printf '%s\n' "$url" >>"$FETCH_LOG"
case "$url" in
*api.github.com*)
[ "${API_MODE:-ok}" = "ok" ] || exit 22
printf '{"tag_name": "%s"}\n' "${API_TAG:-v12.0.0}"
;;
*.sha256)
case "${CHECKSUM_MODE:-good}" in
good) cat "$FIXTURE_DIR/${ARCHIVE_MODE:-good}.sha256" ;;
bad) printf '%064d fixture.tar.gz\n' 0 ;;
empty) : ;;
fail) exit 22 ;;
*) exit 64 ;;
esac
;;
*.tar.gz)
case "${ARCHIVE_MODE:-good}" in
good) cat "$FIXTURE_DIR/good.tar.gz" ;;
missing) cat "$FIXTURE_DIR/missing.tar.gz" ;;
bad-version) cat "$FIXTURE_DIR/bad-version.tar.gz" ;;
cannot-run) cat "$FIXTURE_DIR/cannot-run.tar.gz" ;;
corrupt) cat "$FIXTURE_DIR/corrupt.tar.gz" ;;
fail) exit 22 ;;
*) exit 64 ;;
esac
;;
*) exit 22 ;;
esac
EOF
chmod +x "$tmp/stub/uname" "$tmp/stub/curl"
base_path="/usr/bin:/bin"
cat >"$tmp/brew-stub" <<'EOF'
#!/bin/sh
printf '%s\n' "$*" >"$BREW_LOG"
[ "${BREW_MODE:-ok}" = ok ] || exit 23
EOF
chmod +x "$tmp/stub/uname" "$tmp/stub/fetch" "$tmp/brew-stub"
ln -s "$tmp/stub/fetch" "$tmp/stub/curl"
base_path="$tmp/stub:/usr/bin:/bin"
fetch_log="$tmp/fetch.log"
uname_log="$tmp/uname.log"
brew_log="$tmp/brew.log"
fixture_install() {
destination=$1
shift
: >"$fetch_log"
STUB_OS="${STUB_OS:-Darwin}" STUB_ARCH="${STUB_ARCH:-arm64}" \
FETCH_LOG="$fetch_log" FIXTURE_DIR="$tmp/fixtures" \
SOULSEEK_RS_INSTALL_DIR="$destination" PATH="$base_path" \
/bin/sh "$script" "$@"
}
# Argument parsing happens before platform detection, including when piped via
# `sh -s --`. Help must therefore work even on an unsupported machine.
: >"$uname_log"
out=$(STUB_OS=Plan9 STUB_ARCH=mips UNAME_LOG="$uname_log" PATH="$base_path" \
/bin/sh "$script" --help 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -eq 0 ] && contains "$out" "sh -s -- --nightly" && [ ! -s "$uname_log" ]; then res=0; fi
report "$res" "--help works without probing the machine"
out=$(PATH="$base_path" /bin/sh "$script" --unknown 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "unknown option '--unknown'"; then res=0; fi
report "$res" "unknown options fail with guidance"
out=$(PATH="$base_path" /bin/sh "$script" -- nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "unexpected argument 'nightly'"; then res=0; fi
report "$res" "positional arguments are rejected"
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
: >"$fetch_log"
STUB_OS="$1" STUB_ARCH="$2" ARCHIVE_MODE=fail FETCH_LOG="$fetch_log" \
FIXTURE_DIR="$tmp/fixtures" SOULSEEK_RS_INSTALL_DIR="$tmp/detection" \
PATH="$base_path" /bin/sh "$script" >/dev/null 2>&1 || true
sed -n 's#^.*/\(soulseek-rs-v12\.0\.0-[a-z0-9_-]*\.tar\.gz\)$#\1#p' "$fetch_log" | head -n1
}
for spec in \
@@ -54,102 +154,246 @@ for spec in \
"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
# The record deliberately relies on POSIX field splitting.
# 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"
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=$?
out=$(STUB_OS=MINGW64_NT-10.0 STUB_ARCH=x86_64 PATH="$base_path" \
/bin/sh "$script" --nightly 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"
if [ "$rc" -ne 0 ] && contains "$out" "pc-windows-msvc" && contains "$out" "releases/tag/nightly"; then res=0; fi
report "$res" "Windows refuses and links to the selected channel"
out=$(STUB_OS=Linux STUB_ARCH=armv7l PATH="$tmp/stub:$base_path" sh "$script" 2>&1) && rc=0 || rc=$?
out=$(STUB_OS=Linux STUB_ARCH=armv7l PATH="$base_path" /bin/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"
if [ "$rc" -ne 0 ] && contains "$out" "unsupported architecture 'armv7l'"; then res=0; fi
report "$res" "unsupported architectures fail before downloading"
# 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
out=$(STUB_OS=Plan9 STUB_ARCH=x86_64 PATH="$base_path" /bin/sh "$script" 2>&1) && rc=0 || rc=$?
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"
if [ "$rc" -ne 0 ] && contains "$out" "unsupported OS 'Plan9'"; then res=0; fi
report "$res" "unsupported operating systems fail before downloading"
# 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 ;;
# Stable uses Homebrew when available. Nightly must bypass it because the tap
# intentionally tracks stable releases only.
mkdir -p "$tmp/with-brew"
ln -s "$tmp/stub/uname" "$tmp/with-brew/uname"
ln -s "$tmp/brew-stub" "$tmp/with-brew/brew"
ln -s "$tmp/stub/fetch" "$tmp/with-brew/curl"
: >"$fetch_log"
BREW_LOG="$brew_log" STUB_OS=Darwin STUB_ARCH=arm64 FETCH_LOG="$fetch_log" \
PATH="$tmp/with-brew:/usr/bin:/bin" /bin/sh "$script" >/dev/null 2>&1
res=1
if [ "$(cat "$brew_log")" = "install michel/tap/soulseek-rs" ] && [ ! -s "$fetch_log" ]; then res=0; fi
report "$res" "stable uses Homebrew without touching GitHub"
out=$(BREW_MODE=fail BREW_LOG="$brew_log" STUB_OS=Darwin STUB_ARCH=arm64 \
PATH="$tmp/with-brew:/usr/bin:/bin" /bin/sh "$script" 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -eq 23 ]; then res=0; fi
report "$res" "Homebrew failures propagate to the caller"
nightly_dir="$tmp/nightly install/bin"
: >"$brew_log"
out=$(BREW_LOG="$brew_log" STUB_OS=Darwin STUB_ARCH=arm64 \
FETCH_LOG="$fetch_log" FIXTURE_DIR="$tmp/fixtures" \
SOULSEEK_RS_INSTALL_DIR="$nightly_dir" PATH="$tmp/with-brew:/usr/bin:/bin" \
/bin/sh "$script" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -eq 0 ] && [ -x "$nightly_dir/soulseek-rs" ] &&
contains "$out" "(nightly)" && [ ! -s "$brew_log" ] &&
! grep -q 'api.github.com' "$fetch_log" &&
grep -q '/nightly/soulseek-rs-nightly-aarch64-apple-darwin.tar.gz$' "$fetch_log"; then res=0; fi
report "$res" "--nightly bypasses Homebrew and the stable-release API"
custom_stable_dir="$tmp/custom stable/bin"
: >"$brew_log"
out=$(BREW_LOG="$brew_log" STUB_OS=Darwin STUB_ARCH=arm64 \
FETCH_LOG="$fetch_log" FIXTURE_DIR="$tmp/fixtures" \
SOULSEEK_RS_INSTALL_DIR="$custom_stable_dir" PATH="$tmp/with-brew:/usr/bin:/bin" \
/bin/sh "$script" 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -eq 0 ] && [ -x "$custom_stable_dir/soulseek-rs" ] &&
[ ! -s "$brew_log" ] && grep -q 'api.github.com' "$fetch_log"; then res=0; fi
report "$res" "custom install directory overrides Homebrew"
stable_dir="$tmp/stable/bin"
out=$(fixture_install "$stable_dir" 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -eq 0 ] && [ -x "$stable_dir/soulseek-rs" ] &&
[ "$("$stable_dir/soulseek-rs" --version)" = "soulseek-rs 99.0.0-test" ] &&
contains "$out" "(stable)" &&
grep -q '/v12.0.0/soulseek-rs-v12.0.0-aarch64-apple-darwin.tar.gz$' "$fetch_log"; then res=0; fi
report "$res" "verified stable archive installs and runs"
res=1
if contains "$out" "$stable_dir is not on your PATH"; then res=0; fi
report "$res" "a destination off PATH gets a usable PATH hint"
# A minimal PATH with wget but no curl proves the fallback instead of merely
# exercising a wget-shaped stub while curl still wins command discovery.
mkdir -p "$tmp/wget-path"
for tool in cat chmod cp gzip head mkdir mktemp mv rm sed shasum sha256sum tar; do
tool_path=$(command -v "$tool" 2>/dev/null || true)
[ -z "$tool_path" ] || ln -s "$tool_path" "$tmp/wget-path/$tool"
done
ln -s "$tmp/stub/uname" "$tmp/wget-path/uname"
ln -s "$tmp/stub/fetch" "$tmp/wget-path/wget"
wget_dir="$tmp/wget/bin"
: >"$fetch_log"
out=$(STUB_OS=Darwin STUB_ARCH=arm64 FETCH_LOG="$fetch_log" \
FIXTURE_DIR="$tmp/fixtures" SOULSEEK_RS_INSTALL_DIR="$wget_dir" \
PATH="$tmp/wget-path" /bin/sh "$script" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -eq 0 ] && [ -x "$wget_dir/soulseek-rs" ]; then res=0; fi
report "$res" "wget-only installation works"
# Failures before replacement must preserve an existing install and leave no
# fixed-name staging file behind.
preserve_dir="$tmp/preserve/bin"
mkdir -p "$preserve_dir"
printf '#!/bin/sh\nprintf "soulseek-rs old\\n"\n' >"$preserve_dir/soulseek-rs"
chmod +x "$preserve_dir/soulseek-rs"
out=$(CHECKSUM_MODE=bad fixture_install "$preserve_dir" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "sha256 mismatch" &&
[ "$("$preserve_dir/soulseek-rs")" = "soulseek-rs old" ]; then res=0; fi
report "$res" "checksum mismatch preserves the installed binary"
out=$(CHECKSUM_MODE=empty fixture_install "$preserve_dir" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "checksum" && contains "$out" "empty"; then res=0; fi
report "$res" "empty checksum is rejected"
out=$(ARCHIVE_MODE=missing fixture_install "$preserve_dir" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "did not contain a soulseek-rs binary" &&
[ "$("$preserve_dir/soulseek-rs")" = "soulseek-rs old" ]; then res=0; fi
report "$res" "archive without the binary preserves the installed binary"
out=$(ARCHIVE_MODE=bad-version fixture_install "$preserve_dir" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "unexpected version output" &&
[ "$("$preserve_dir/soulseek-rs")" = "soulseek-rs old" ]; then res=0; fi
report "$res" "wrong executable preserves the installed binary"
out=$(ARCHIVE_MODE=cannot-run fixture_install "$preserve_dir" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "could not run on this machine" &&
[ "$("$preserve_dir/soulseek-rs")" = "soulseek-rs old" ]; then res=0; fi
report "$res" "non-running executable preserves the installed binary"
out=$(ARCHIVE_MODE=corrupt fixture_install "$preserve_dir" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "could not unpack" &&
[ "$("$preserve_dir/soulseek-rs")" = "soulseek-rs old" ]; then res=0; fi
report "$res" "corrupt archive preserves the installed binary"
out=$(ARCHIVE_MODE=fail fixture_install "$preserve_dir" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "could not download soulseek-rs-nightly" &&
[ "$("$preserve_dir/soulseek-rs")" = "soulseek-rs old" ]; then res=0; fi
report "$res" "archive download failure preserves the installed binary"
out=$(CHECKSUM_MODE=fail fixture_install "$preserve_dir" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "could not download the sha256 checksum" &&
[ "$("$preserve_dir/soulseek-rs")" = "soulseek-rs old" ]; then res=0; fi
report "$res" "checksum download failure preserves the installed binary"
directory_target="$tmp/directory-target/bin"
mkdir -p "$directory_target/soulseek-rs"
out=$(fixture_install "$directory_target" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "is a directory" &&
[ ! -e "$directory_target/soulseek-rs/.soulseek-rs.new" ]; then res=0; fi
report "$res" "a directory at the binary path is never written into"
out=$(API_MODE=fail fixture_install "$tmp/api-failure" 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "could not resolve the latest release"; then res=0; fi
report "$res" "stable API failure is explained"
: >"$fetch_log"
out=$(API_TAG='../nightly' fixture_install "$tmp/unsafe-tag" 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "unexpected tag" &&
[ "$(wc -l <"$fetch_log" | tr -d ' ')" = 1 ]; then res=0; fi
report "$res" "unexpected stable tag is rejected before asset download"
mkdir -p "$tmp/no-fetch"
ln -s "$tmp/stub/uname" "$tmp/no-fetch/uname"
out=$(STUB_OS=Darwin STUB_ARCH=arm64 PATH="$tmp/no-fetch" /bin/sh "$script" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "need curl or wget"; then res=0; fi
report "$res" "missing downloader is reported"
mkdir -p "$tmp/no-tar"
ln -s "$tmp/stub/uname" "$tmp/no-tar/uname"
ln -s "$tmp/stub/fetch" "$tmp/no-tar/curl"
out=$(STUB_OS=Darwin STUB_ARCH=arm64 PATH="$tmp/no-tar" /bin/sh "$script" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "need tar"; then res=0; fi
report "$res" "missing tar is reported before downloading"
mkdir -p "$tmp/no-sha"
ln -s "$tmp/stub/uname" "$tmp/no-sha/uname"
ln -s "$tmp/stub/fetch" "$tmp/no-sha/curl"
tar_path=$(command -v tar)
ln -s "$tar_path" "$tmp/no-sha/tar"
out=$(STUB_OS=Darwin STUB_ARCH=arm64 PATH="$tmp/no-sha" /bin/sh "$script" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -ne 0 ] && contains "$out" "need sha256sum or shasum"; then res=0; fi
report "$res" "missing checksum tool is reported before downloading"
# Two callers can update the same path without sharing a predictable staging
# name. Either can win; the result must run and no staging file may remain.
concurrent_dir="$tmp/concurrent/bin"
fixture_install "$concurrent_dir" --nightly >/dev/null 2>&1 & first=$!
fixture_install "$concurrent_dir" --nightly >/dev/null 2>&1 & second=$!
first_rc=0
second_rc=0
wait "$first" || first_rc=$?
wait "$second" || second_rc=$?
set -- "$concurrent_dir"/.soulseek-rs.*
res=1
if [ "$first_rc" -eq 0 ] && [ "$second_rc" -eq 0 ] &&
[ "$("$concurrent_dir/soulseek-rs" --version)" = "soulseek-rs 99.0.0-test" ] &&
[ ! -e "$1" ]; then res=0; fi
report "$res" "concurrent installers leave one complete binary"
case "${INSTALL_TEST_NETWORK:-}" in
"") ;;
stable | all)
network_path="/usr/bin:/bin"
live_stable="$tmp/live-stable"
out=$(SOULSEEK_RS_INSTALL_DIR="$live_stable" PATH="$network_path" \
/bin/sh "$script" 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -eq 0 ] && "$live_stable/soulseek-rs" --version >/dev/null 2>&1; then res=0; fi
report "$res" "live stable release installs and runs"
if [ "${INSTALL_TEST_NETWORK:-}" = all ]; then
live_nightly="$tmp/live-nightly"
out=$(SOULSEEK_RS_INSTALL_DIR="$live_nightly" PATH="$network_path" \
/bin/sh "$script" --nightly 2>&1) && rc=0 || rc=$?
res=1
if [ "$rc" -eq 0 ] && "$live_nightly/soulseek-rs" --version >/dev/null 2>&1; then res=0; fi
report "$res" "live nightly release installs and runs"
fi
;;
*)
printf 'FAIL INSTALL_TEST_NETWORK must be empty, stable, or all\n'
failures=$((failures + 1))
;;
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"
[ "$failures" -eq 0 ] || {
printf '%s test(s) failed\n' "$failures"
exit 1
}
echo "all tests passed"
printf 'all tests passed\n'
+3
View File
@@ -2,6 +2,7 @@ export const LINKS = {
gh: 'https://github.com/michel/soulseek-rs',
issues: 'https://github.com/michel/soulseek-rs/issues',
releases: 'https://github.com/michel/soulseek-rs/releases',
nightlyRelease: 'https://github.com/michel/soulseek-rs/releases/tag/nightly',
changelog: 'https://github.com/michel/soulseek-rs/blob/master/CHANGELOG.md',
contributing: 'https://github.com/michel/soulseek-rs/blob/master/CONTRIBUTING.md',
license: 'https://github.com/michel/soulseek-rs/blob/master/LICENSE',
@@ -25,3 +26,5 @@ 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`
export const NIGHTLY_INSTALL_CMD = `${INSTALL_CMD} -s -- --nightly`
+20 -5
View File
@@ -6,7 +6,14 @@ 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 { INSTALL_CMD, LINKS, SITE_URL } from '@/lib/links'
import { INSTALL_CMD, LINKS, NIGHTLY_INSTALL_CMD, SITE_URL } from '@/lib/links'
const INSTALL_SCRIPT_LINES: readonly TermLine[] = [
{ t: 'cm', text: '# latest stable release' },
{ t: 'cmd', text: INSTALL_CMD },
{ t: 'cm', text: '# latest successful build from develop' },
{ t: 'cmd', text: NIGHTLY_INSTALL_CMD },
]
const LIB_SRC = `use soulseek_rs::Client;
use std::time::Duration;
@@ -266,16 +273,23 @@ const Steps = () => (
<div className="flex flex-col gap-9">
<Step n={1} title="Install the client">
<p className="text-secondary">
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.
One command on macOS and Linux. The script installs the latest stable release
through Homebrew when you have it, or downloads the prebuilt binary, checks its
sha256, and puts it on your PATH. Pass <Code>--nightly</Code> for the current
successful build from <Code>develop</Code>; nightly always downloads directly.
</p>
<Terminal lines={[{ t: 'cmd', text: INSTALL_CMD }]} wrap />
<Terminal lines={INSTALL_SCRIPT_LINES} 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-[13px] text-muted">
Nightly is rebuilt every day and can be replaced by a manual build. It may be
less stable than a numbered release. The installer stages and runs the verified
download before replacing an existing copy, so a bad build leaves that copy in
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.
@@ -316,6 +330,7 @@ const Steps = () => (
</p>
<div className="flex flex-wrap gap-2 sm:gap-3">
<Button href={LINKS.releases}>Prebuilt binaries</Button>
<Button href={LINKS.nightlyRelease}>Nightly binaries</Button>
</div>
</Step>