feat(ci): publish installable nightly builds

This commit is contained in:
Michel de Graaf
2026-09-08 13:40:50 +02:00
parent af1f9241e4
commit 99205026f8
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 - name: Install
run: bun install --frozen-lockfile run: bun install --frozen-lockfile
- name: Test installer
run: bun run test:install
- name: Typecheck - name: Typecheck
run: bun run typecheck run: bun run typecheck
+21 -1
View File
@@ -76,7 +76,27 @@ project is identifiable on the network.
## Installation ## 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 ```bash
brew install michel/tap/soulseek-rs # macOS and Linux 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`. 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 ## One-time repository setup
- **Settings → Actions → General → Workflow permissions**: enable *Allow GitHub Actions to - **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", "build:ssr": "vite build --ssr src/entry-server.tsx --outDir dist-ssr",
"prerender": "bun scripts/prerender.ts", "prerender": "bun scripts/prerender.ts",
"preview": "vite preview", "preview": "vite preview",
"test:install": "sh scripts/test-install.sh",
"typecheck": "tsc -b", "typecheck": "tsc -b",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix" "lint:fix": "eslint . --fix"
+117 -33
View File
@@ -1,11 +1,15 @@
#!/bin/sh #!/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 # curl -fsSL https://re-invention.nl/soulseek-rs/install.sh | sh
# #
# Uses Homebrew when it is on the PATH (macOS and Linux), otherwise downloads # Latest successful build from develop:
# the prebuilt binary for this platform from GitHub releases, verifies its # curl -fsSL https://re-invention.nl/soulseek-rs/install.sh | sh -s -- --nightly
# sha256, and puts it in /usr/local/bin or ~/.local/bin. #
# 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. # SOULSEEK_RS_INSTALL_DIR overrides where the binary lands.
set -eu set -eu
@@ -17,23 +21,66 @@ die() {
printf 'install.sh: %s\n' "$*" >&2 printf 'install.sh: %s\n' "$*" >&2
exit 1 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" ;; Darwin) os="apple-darwin" ;;
Linux) os="unknown-linux-musl" ;; Linux) os="unknown-linux-musl" ;;
MINGW* | MSYS* | CYGWIN* | Windows_NT) 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 "no install script for Windows: take the pc-windows-msvc zip from $release_page, or run 'cargo install soulseek-rs'" ;;
*) die "unsupported OS '$(uname -s)': see https://github.com/$REPO/releases/latest" ;; *) die "unsupported OS '$system': see $release_page" ;;
esac esac
case "$(uname -m)" in machine=$(uname -m)
case "$machine" in
x86_64 | amd64) arch="x86_64" ;; x86_64 | amd64) arch="x86_64" ;;
arm64 | aarch64) arch="aarch64" ;; 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 esac
target="$arch-$os" 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)" say "Homebrew found: installing michel/tap/soulseek-rs (brew also handles upgrades)"
exec brew install michel/tap/soulseek-rs exec brew install michel/tap/soulseek-rs
fi fi
@@ -46,47 +93,84 @@ else
die "need curl or wget" die "need curl or wget"
fi fi
tag=$(fetch "https://api.github.com/repos/$REPO/releases/latest" | command -v tar >/dev/null 2>&1 || die "need tar to unpack the download"
sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n1) if command -v sha256sum >/dev/null 2>&1; then
[ -n "$tag" ] || die "could not resolve the latest release; downloads are at https://github.com/$REPO/releases/latest" 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" archive="soulseek-rs-$tag-$target.tar.gz"
base_url="https://github.com/$REPO/releases/download/$tag" base_url="https://github.com/$REPO/releases/download/$tag"
tmp=$(mktemp -d) tmp=""
trap 'rm -rf "$tmp"' EXIT 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" say "Downloading $archive"
fetch "$base_url/$archive" >"$tmp/$archive" 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" 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=$(sha256 "$tmp/$archive") || die "could not calculate the sha256 for $archive"
got=$(sha256sum "$tmp/$archive")
else
got=$(shasum -a 256 "$tmp/$archive")
fi
got=${got%% *} 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" [ "$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" [ -f "$tmp/soulseek-rs" ] || die "archive $archive did not contain a soulseek-rs binary"
dir="${SOULSEEK_RS_INSTALL_DIR:-}" dir="${SOULSEEK_RS_INSTALL_DIR:-}"
if [ -z "$dir" ]; then if [ -z "$dir" ]; then
dir="/usr/local/bin" 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 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 staged=$(mktemp "$dir/.soulseek-rs.XXXXXX") ||
# fails with "text file busy" on Linux. die "could not create a temporary file in '$dir'"
cp "$tmp/soulseek-rs" "$dir/.soulseek-rs.new" cp "$tmp/soulseek-rs" "$staged" || die "could not stage the downloaded binary in '$dir'"
chmod 755 "$dir/.soulseek-rs.new" chmod 755 "$staged" || die "could not make the downloaded binary executable"
mv -f "$dir/.soulseek-rs.new" "$dir/soulseek-rs" 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" mv -f "$staged" "$dir/soulseek-rs" || die "could not replace '$dir/soulseek-rs'"
case ":$PATH:" in staged=""
say "Installed $version ($channel) to $dir/soulseek-rs"
case ":${PATH:-}:" in
*:"$dir":*) ;; *:"$dir":*) ;;
*) say "note: $dir is not on your PATH; add it with: export PATH=\"$dir:\$PATH\"" ;; *) say "note: $dir is not on your PATH; add it with: export PATH=\"$dir:\$PATH\"" ;;
esac esac
+347 -103
View File
@@ -1,17 +1,17 @@
#!/bin/sh #!/bin/sh
# Exercises public/install.sh: target detection, the brew fast path, checksum # Hermetic coverage for public/install.sh. Set INSTALL_TEST_NETWORK=stable to
# verification, and a real install into a throwaway prefix. Network tests hit # add a live stable-release install, or =all after a nightly release exists to
# GitHub; run from a machine that can reach it. # exercise both public channels.
set -eu set -eu
here=$(cd "$(dirname "$0")" && pwd) here=$(cd "$(dirname "$0")" && pwd)
script="$here/../public/install.sh" script="$here/../public/install.sh"
tmp=$(mktemp -d) tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT trap 'rm -rf "$tmp"' EXIT HUP INT TERM
failures=0 failures=0
report() { report() {
if [ "$1" = 0 ]; then if [ "$1" -eq 0 ]; then
printf 'ok %s\n' "$2" printf 'ok %s\n' "$2"
else else
printf 'FAIL %s\n' "$2" printf 'FAIL %s\n' "$2"
@@ -20,33 +20,133 @@ report() {
} }
contains() { case "$1" in *"$2"*) true ;; *) false ;; esac; } contains() { case "$1" in *"$2"*) true ;; *) false ;; esac; }
# Stub uname reads STUB_OS / STUB_ARCH; stub curl logs every URL it is asked if command -v sha256sum >/dev/null 2>&1; then
# for, answers the release-API call with a canned tag, and refuses the rest so fixture_sha256() { sha256sum "$1"; }
# a detection test stops before downloading anything. 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" mkdir -p "$tmp/stub"
cat >"$tmp/stub/uname" <<'EOF' cat >"$tmp/stub/uname" <<'EOF'
#!/bin/sh #!/bin/sh
[ -z "${UNAME_LOG:-}" ] || printf '%s\n' "${1:-system}" >>"$UNAME_LOG"
case "${1:-}" in case "${1:-}" in
-m) echo "$STUB_ARCH" ;; -m) printf '%s\n' "$STUB_ARCH" ;;
*) echo "$STUB_OS" ;; *) printf '%s\n' "$STUB_OS" ;;
esac esac
EOF EOF
cat >"$tmp/stub/curl" <<EOF cat >"$tmp/stub/fetch" <<'EOF'
#!/bin/sh #!/bin/sh
for a; do url=\$a; done url=""
echo "\$url" >>"$tmp/curl.log" for argument do url=$argument; done
case "\$url" in [ -z "${FETCH_LOG:-}" ] || printf '%s\n' "$url" >>"$FETCH_LOG"
*api.github.com*) echo '{"tag_name": "v12.0.0"}' ;; 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 ;; *) exit 22 ;;
esac esac
EOF EOF
chmod +x "$tmp/stub/uname" "$tmp/stub/curl" cat >"$tmp/brew-stub" <<'EOF'
base_path="/usr/bin:/bin" #!/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() { resolved_target() {
: >"$tmp/curl.log" : >"$fetch_log"
STUB_OS="$1" STUB_ARCH="$2" PATH="$tmp/stub:$base_path" sh "$script" >/dev/null 2>&1 || true STUB_OS="$1" STUB_ARCH="$2" ARCHIVE_MODE=fail FETCH_LOG="$fetch_log" \
grep -o 'soulseek-rs-v12\.0\.0-[a-z0-9_-]*\.tar\.gz' "$tmp/curl.log" | head -n1 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 \ for spec in \
@@ -54,102 +154,246 @@ for spec in \
"Darwin x86_64 x86_64-apple-darwin" \ "Darwin x86_64 x86_64-apple-darwin" \
"Linux aarch64 aarch64-unknown-linux-musl" \ "Linux aarch64 aarch64-unknown-linux-musl" \
"Linux amd64 x86_64-unknown-linux-musl"; do "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 # shellcheck disable=SC2086
set -- $spec set -- $spec
res=1 res=1
if [ "$(resolved_target "$1" "$2")" = "soulseek-rs-v12.0.0-$3.tar.gz" ]; then res=0; fi 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 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 res=1
if [ "$rc" != 0 ] && contains "$out" "pc-windows-msvc"; then res=0; fi if [ "$rc" -ne 0 ] && contains "$out" "pc-windows-msvc" && contains "$out" "releases/tag/nightly"; then res=0; fi
report $res "Windows refuses and points at the msvc zip" 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 res=1
if [ "$rc" != 0 ] && contains "$out" "unsupported architecture"; then res=0; fi if [ "$rc" -ne 0 ] && contains "$out" "unsupported architecture 'armv7l'"; then res=0; fi
report $res "armv7l refuses as unsupported" report "$res" "unsupported architectures fail before downloading"
# Brew fast path: a stub brew must be exec'd with the tap formula, and no URL out=$(STUB_OS=Plan9 STUB_ARCH=x86_64 PATH="$base_path" /bin/sh "$script" 2>&1) && rc=0 || rc=$?
# 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 res=1
if [ "$(cat "$tmp/brew.log")" = "install michel/tap/soulseek-rs" ] && [ ! -s "$tmp/curl.log" ]; then res=0; fi if [ "$rc" -ne 0 ] && contains "$out" "unsupported OS 'Plan9'"; then res=0; fi
report $res "brew on PATH wins and skips the download" report "$res" "unsupported operating systems fail before downloading"
rm -f "$tmp/stub/brew"
# Offline happy path: stub curl serves a locally built archive whose sha256 # Stable uses Homebrew when available. Nightly must bypass it because the tap
# matches, and the installed fake binary must answer --version. # intentionally tracks stable releases only.
mkdir -p "$tmp/payload" "$tmp/bin" mkdir -p "$tmp/with-brew"
printf '#!/bin/sh\necho "soulseek-rs 99.0.0-test"\n' >"$tmp/payload/soulseek-rs" ln -s "$tmp/stub/uname" "$tmp/with-brew/uname"
chmod +x "$tmp/payload/soulseek-rs" ln -s "$tmp/brew-stub" "$tmp/with-brew/brew"
tar -czf "$tmp/payload.tar.gz" -C "$tmp/payload" soulseek-rs ln -s "$tmp/stub/fetch" "$tmp/with-brew/curl"
sum=$(shasum -a 256 "$tmp/payload.tar.gz" | cut -d' ' -f1) : >"$fetch_log"
cat >"$tmp/stub/curl" <<EOF BREW_LOG="$brew_log" STUB_OS=Darwin STUB_ARCH=arm64 FETCH_LOG="$fetch_log" \
#!/bin/sh PATH="$tmp/with-brew:/usr/bin:/bin" /bin/sh "$script" >/dev/null 2>&1
for a; do url=\$a; done res=1
echo "\$url" >>"$tmp/curl.log" if [ "$(cat "$brew_log")" = "install michel/tap/soulseek-rs" ] && [ ! -s "$fetch_log" ]; then res=0; fi
case "\$url" in report "$res" "stable uses Homebrew without touching GitHub"
*api.github.com*) echo '{"tag_name": "v12.0.0"}' ;;
*.tar.gz) cat "$tmp/payload.tar.gz" ;; out=$(BREW_MODE=fail BREW_LOG="$brew_log" STUB_OS=Darwin STUB_ARCH=arm64 \
*.sha256) echo "$sum *payload" ;; PATH="$tmp/with-brew:/usr/bin:/bin" /bin/sh "$script" 2>&1) && rc=0 || rc=$?
*) exit 22 ;; 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 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. [ "$failures" -eq 0 ] || {
rm -f "$tmp/bin/soulseek-rs" printf '%s test(s) failed\n' "$failures"
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 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', gh: 'https://github.com/michel/soulseek-rs',
issues: 'https://github.com/michel/soulseek-rs/issues', issues: 'https://github.com/michel/soulseek-rs/issues',
releases: 'https://github.com/michel/soulseek-rs/releases', 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', changelog: 'https://github.com/michel/soulseek-rs/blob/master/CHANGELOG.md',
contributing: 'https://github.com/michel/soulseek-rs/blob/master/CONTRIBUTING.md', contributing: 'https://github.com/michel/soulseek-rs/blob/master/CONTRIBUTING.md',
license: 'https://github.com/michel/soulseek-rs/blob/master/LICENSE', 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 SITE_URL = 'https://re-invention.nl/soulseek-rs/'
export const INSTALL_CMD = `curl -fsSL ${SITE_URL}install.sh | sh` 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 { OsIcon, type OsName } from '@/components/ui/os-icon'
import { Callout } from '@/components/ui/panel' import { Callout } from '@/components/ui/panel'
import { Terminal, type TermLine } from '@/components/ui/terminal' 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; const LIB_SRC = `use soulseek_rs::Client;
use std::time::Duration; use std::time::Duration;
@@ -266,16 +273,23 @@ const Steps = () => (
<div className="flex flex-col gap-9"> <div className="flex flex-col gap-9">
<Step n={1} title="Install the client"> <Step n={1} title="Install the client">
<p className="text-secondary"> <p className="text-secondary">
One command on macOS and Linux. The script installs through Homebrew when you One command on macOS and Linux. The script installs the latest stable release
have it; otherwise it downloads the latest release binary for your platform, through Homebrew when you have it, or downloads the prebuilt binary, checks its
checks its sha256, and puts it on your PATH. 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> </p>
<Terminal lines={[{ t: 'cmd', text: INSTALL_CMD }]} wrap /> <Terminal lines={INSTALL_SCRIPT_LINES} wrap />
<p className="text-[13px] text-muted"> <p className="text-[13px] text-muted">
No curl? <Code>{`wget -qO- ${SITE_URL}install.sh | sh`}</Code> does the same. 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{' '} Piping to a shell asks for trust; read the script first at{' '}
<ExtLink href={`${SITE_URL}install.sh`}>{`${SITE_URL}install.sh`}</ExtLink>. <ExtLink href={`${SITE_URL}install.sh`}>{`${SITE_URL}install.sh`}</ExtLink>.
</p> </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"> <p className="text-secondary">
Or pick a route per platform. Cargo needs a Rust toolchain from{' '} 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. <ExtLink href={LINKS.rustup}>rustup.rs</ExtLink>; the other routes don&rsquo;t.
@@ -316,6 +330,7 @@ const Steps = () => (
</p> </p>
<div className="flex flex-wrap gap-2 sm:gap-3"> <div className="flex flex-wrap gap-2 sm:gap-3">
<Button href={LINKS.releases}>Prebuilt binaries</Button> <Button href={LINKS.releases}>Prebuilt binaries</Button>
<Button href={LINKS.nightlyRelease}>Nightly binaries</Button>
</div> </div>
</Step> </Step>