mirror of
https://github.com/buckit-io/buckit.git
synced 2026-09-11 21:39:03 +00:00
0801e31164
install-linux-binary.sh and install-mac.sh were 443 lines that differed in three places: the OS check and architecture allowlist, the platform token in the pointer URL and asset name, and clearing the macOS quarantine attribute. The other ~190 lines were identical. That duplication already cost something. Hardening the installers meant applying five fixes twice, by hand, in parallel -- pin-bypass path traversal, pipeline masking, digest normalisation, directory destination, and the cross-origin digest check. The next fix would have had the same shape, and eventually one would land in only one file. Merge them into install-binary.sh, which detects Linux or macOS and validates the architecture against what is published for that platform: Linux ships amd64 and arm64, macOS ships Apple Silicon only. The release workflow publishes it under the old names as well, so URLs already in the wild, in the docs, and in the blog post keep working and pick up the merged behaviour on every release. Nothing needs to change on the reader's side, and there is no window where a documented command 404s. Two benign consequences: install-mac.sh now also works on Linux and install-linux-binary.sh on macOS, and error messages self-identify as install-binary.sh whichever URL was fetched.
238 lines
8.1 KiB
Bash
Executable File
238 lines
8.1 KiB
Bash
Executable File
#!/bin/sh
|
|
# install-binary.sh — standalone-binary installer helper for buckit.
|
|
#
|
|
# Downloads the buckit binary for this host (Linux or macOS) for the latest
|
|
# stable release to a predictable filename (buckit), verifies its published
|
|
# SHA-256 checksum, and leaves it in place so you can run it directly. It does
|
|
# NOT install it onto your PATH, and it does NOT register a service.
|
|
#
|
|
# On Linux, for a package-managed install with a systemd service, use
|
|
# install-linux.sh instead, which downloads the .deb/.rpm/.apk for this host.
|
|
#
|
|
# Also published as install-linux-binary.sh and install-mac.sh, the per-platform
|
|
# names this script replaced. Those URLs serve this same script.
|
|
#
|
|
# Usage:
|
|
# curl -fsSL https://buckit-io.github.io/buckit/install-binary.sh | sh
|
|
# ./buckit --help # run it from where it was downloaded
|
|
#
|
|
# Environment overrides:
|
|
# BUCKIT_PAGES_BASE gh-pages base URL
|
|
# (default: https://buckit-io.github.io/buckit)
|
|
# BUCKIT_RELEASE_BASE release download base
|
|
# (default: https://github.com/buckit-io/buckit/releases/download)
|
|
# BUCKIT_VERSION pin a release tag (e.g. RELEASE.2026-05-11T17-20-40Z)
|
|
# instead of resolving the latest stable release
|
|
# BUCKIT_DOWNLOAD_DIR directory to download the binary into
|
|
# (default: the current directory)
|
|
|
|
set -eu
|
|
|
|
PAGES_BASE="${BUCKIT_PAGES_BASE:-https://buckit-io.github.io/buckit}"
|
|
RELEASE_BASE="${BUCKIT_RELEASE_BASE:-https://github.com/buckit-io/buckit/releases/download}"
|
|
|
|
# Set by resolve_release.
|
|
TAG=""
|
|
POINTER_SHA=""
|
|
|
|
err() {
|
|
echo "install-binary.sh: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
info() {
|
|
echo "==> $*"
|
|
}
|
|
|
|
# detect_platform sets OS and ARCH to the Go-style tuple naming the published
|
|
# release assets, and validates the architecture against what is published for
|
|
# that platform: Linux ships amd64 and arm64, macOS ships Apple Silicon only.
|
|
detect_platform() {
|
|
uname_s="$(uname -s)"
|
|
case "$uname_s" in
|
|
Linux) OS="linux" ;;
|
|
Darwin) OS="darwin" ;;
|
|
*) err "this installer is for Linux and macOS (detected '$uname_s'). On Windows use install-windows.ps1" ;;
|
|
esac
|
|
|
|
uname_m="$(uname -m)"
|
|
case "$OS:$uname_m" in
|
|
linux:x86_64 | linux:amd64) ARCH="amd64" ;;
|
|
linux:aarch64 | linux:arm64) ARCH="arm64" ;;
|
|
darwin:arm64 | darwin:aarch64) ARCH="arm64" ;;
|
|
darwin:x86_64 | darwin:amd64) err "unsupported architecture '$uname_m'. Only Apple Silicon (arm64) builds are published for macOS." ;;
|
|
*) err "unsupported architecture '$uname_m' on $OS." ;;
|
|
esac
|
|
}
|
|
|
|
# validate_tag rejects anything that is not a plain release identifier. Both
|
|
# the pinned BUCKIT_VERSION and the pointer-resolved tag go through this, so a
|
|
# value like '../../evil' cannot reach the download URL as path traversal.
|
|
validate_tag() {
|
|
case "$1" in
|
|
RELEASE.?*) ;;
|
|
*) err "unexpected release tag '$1' (expected RELEASE.*)" ;;
|
|
esac
|
|
# Only the characters real release tags use. Rejects '/', whitespace,
|
|
# control characters, and URL delimiters such as '?' and '#'.
|
|
case "$1" in
|
|
*[!A-Za-z0-9._-]*) err "release tag '$1' contains unsupported characters" ;;
|
|
esac
|
|
}
|
|
|
|
# normalize_sha lowercases a hex digest and requires exactly 64 hex characters,
|
|
# so a truncated or malformed checksum record can never be compared as if it
|
|
# were a valid digest. Echoes the normalized digest.
|
|
normalize_sha() {
|
|
_sha="$(printf '%s' "$1" | tr 'ABCDEF' 'abcdef')"
|
|
case "$_sha" in
|
|
"" | *[!0-9a-f]*) err "malformed sha256 digest: '$1'" ;;
|
|
esac
|
|
[ "${#_sha}" -eq 64 ] || err "malformed sha256 digest: '$1'"
|
|
printf '%s' "$_sha"
|
|
}
|
|
|
|
# fetch URL -> stdout
|
|
fetch() {
|
|
if command -v curl >/dev/null 2>&1; then
|
|
curl -fsSL "$1"
|
|
elif command -v wget >/dev/null 2>&1; then
|
|
wget -qO- "$1"
|
|
else
|
|
err "need curl or wget to download"
|
|
fi
|
|
}
|
|
|
|
# fetch_to URL FILE
|
|
fetch_to() {
|
|
if command -v curl >/dev/null 2>&1; then
|
|
curl -fsSL "$1" -o "$2"
|
|
elif command -v wget >/dev/null 2>&1; then
|
|
wget -qO "$2" "$1"
|
|
else
|
|
err "need curl or wget to download"
|
|
fi
|
|
}
|
|
|
|
# sha256_of FILE -> normalized hex digest on stdout. The hash utility runs on
|
|
# its own rather than inside a pipeline, so a failure surfaces instead of being
|
|
# masked by the exit status of a downstream parser.
|
|
sha256_of() {
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
_hash_out="$(sha256sum "$1")" || err "sha256sum failed on $1"
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
_hash_out="$(shasum -a 256 "$1")" || err "shasum failed on $1"
|
|
else
|
|
err "need sha256sum or shasum to verify the download"
|
|
fi
|
|
normalize_sha "$(printf '%s\n' "$_hash_out" | awk 'NR==1{print $1}')"
|
|
}
|
|
|
|
# resolve_release sets TAG, and POINTER_SHA when the release was resolved from
|
|
# the gh-pages pointer. A pinned BUCKIT_VERSION leaves POINTER_SHA empty: the
|
|
# pointer only ever describes the latest release, so it cannot vouch for an
|
|
# arbitrary pinned version.
|
|
resolve_release() {
|
|
if [ -n "${BUCKIT_VERSION:-}" ]; then
|
|
validate_tag "$BUCKIT_VERSION"
|
|
TAG="$BUCKIT_VERSION"
|
|
POINTER_SHA=""
|
|
return
|
|
fi
|
|
|
|
pointer_url="$PAGES_BASE/server/buckit/release/$OS-$ARCH/buckit.sha256sum"
|
|
# Pointer format: "<sha256> buckit.<tag>"
|
|
pointer="$(fetch "$pointer_url")" || err "could not fetch release pointer at $pointer_url"
|
|
|
|
name="$(printf '%s\n' "$pointer" | awk 'NR==1{print $2}')"
|
|
case "$name" in
|
|
buckit.*) ;;
|
|
*) err "unexpected release pointer payload: $pointer" ;;
|
|
esac
|
|
|
|
TAG="${name#buckit.}"
|
|
validate_tag "$TAG"
|
|
POINTER_SHA="$(normalize_sha "$(printf '%s\n' "$pointer" | awk 'NR==1{print $1}')")"
|
|
}
|
|
|
|
main() {
|
|
detect_platform
|
|
info "platform: $OS-$ARCH"
|
|
|
|
resolve_release
|
|
[ -n "$TAG" ] || err "could not resolve a release tag"
|
|
info "release: $TAG"
|
|
|
|
asset="buckit-$OS-$ARCH.$TAG"
|
|
download_url="$RELEASE_BASE/$TAG/$asset"
|
|
|
|
dldir="${BUCKIT_DOWNLOAD_DIR:-.}"
|
|
mkdir -p "$dldir"
|
|
binfile="$dldir/buckit"
|
|
|
|
# Refuse to run when the destination is a directory. 'mv' would move the
|
|
# temp file inside it and the script would report success while leaving
|
|
# nothing runnable at the path it prints.
|
|
[ ! -d "$binfile" ] || err "$binfile is a directory — remove it or set BUCKIT_DOWNLOAD_DIR"
|
|
|
|
# Download to a temporary sibling and only move it into the predictable
|
|
# path after the checksum verifies, so a failed or interrupted download
|
|
# can never clobber an existing good binary or leave a partial/unverified
|
|
# file at the path the printed command references.
|
|
tmpfile="$(mktemp "$dldir/.buckit.XXXXXX")" || err "could not create temp file in $dldir"
|
|
trap 'rm -f "$tmpfile"' EXIT
|
|
|
|
info "downloading $asset"
|
|
fetch_to "$download_url" "$tmpfile" || err "download failed: $download_url"
|
|
|
|
info "fetching published checksum"
|
|
# Capture the payload first: piping the fetch straight into a parser would
|
|
# hide a failed transfer behind the parser's exit status.
|
|
checksum_payload="$(fetch "$download_url.sha256sum")" ||
|
|
err "could not fetch checksum at $download_url.sha256sum"
|
|
release_sha="$(normalize_sha "$(printf '%s\n' "$checksum_payload" | awk 'NR==1{print $1}')")"
|
|
|
|
# The binary and the checksum beside it come from the same origin, so that
|
|
# digest alone only proves the download was not corrupted in transit. The
|
|
# gh-pages pointer publishes the same digest from a separate origin;
|
|
# when it is available, require the two to agree before trusting either.
|
|
if [ -n "$POINTER_SHA" ]; then
|
|
if [ "$POINTER_SHA" != "$release_sha" ]; then
|
|
err "published digests disagree (pages $POINTER_SHA, release $release_sha) — refusing to continue"
|
|
fi
|
|
want_sha="$POINTER_SHA"
|
|
info "sha256 cross-checked against the release pointer"
|
|
else
|
|
want_sha="$release_sha"
|
|
fi
|
|
|
|
got_sha="$(sha256_of "$tmpfile")"
|
|
if [ "$got_sha" != "$want_sha" ]; then
|
|
err "checksum mismatch (expected $want_sha, got $got_sha) — refusing to continue"
|
|
fi
|
|
info "sha256 verified"
|
|
|
|
chmod 0755 "$tmpfile"
|
|
|
|
# Clear the quarantine attribute so Gatekeeper doesn't block the unsigned
|
|
# binary on first run. macOS only; harmless to skip elsewhere.
|
|
if [ "$OS" = "darwin" ] && command -v xattr >/dev/null 2>&1; then
|
|
xattr -d com.apple.quarantine "$tmpfile" 2>/dev/null || true
|
|
fi
|
|
|
|
mv -f "$tmpfile" "$binfile"
|
|
trap - EXIT
|
|
|
|
echo
|
|
echo "Downloaded and verified:"
|
|
echo " $binfile"
|
|
echo
|
|
echo "Run it from here:"
|
|
echo " \"$binfile\""
|
|
echo
|
|
echo "(Move it onto your PATH to call 'buckit' from anywhere.)"
|
|
echo
|
|
}
|
|
|
|
main "$@"
|