feat(ci): publish installable nightly builds
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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'
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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’t.
|
<ExtLink href={LINKS.rustup}>rustup.rs</ExtLink>; the other routes don’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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user