commit ec8d70a0c516208c2b43b4e795c600d0ed561d15 Author: abuckit Date: Sat Aug 8 01:00:17 2026 +0000 deploy: e3737b39dd768944b887d742714bc7b1b6a9050b diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/README.md b/README.md new file mode 100644 index 000000000..7afeef382 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Buckit release pointer files diff --git a/index.html b/index.html new file mode 100644 index 000000000..6b35141e5 --- /dev/null +++ b/index.html @@ -0,0 +1,43 @@ + + + + + + Buckit Pages + + + +
+
+ Static Pages +

Buckit Pages

+

Static indexes published through GitHub Pages for Buckit server release navigation and self-update discovery.

+
+
+ +
+
+ + diff --git a/install-binary.sh b/install-binary.sh new file mode 100755 index 000000000..2a19765c3 --- /dev/null +++ b/install-binary.sh @@ -0,0 +1,237 @@ +#!/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: " buckit." + 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 "$@" diff --git a/install-linux-binary.sh b/install-linux-binary.sh new file mode 100755 index 000000000..2a19765c3 --- /dev/null +++ b/install-linux-binary.sh @@ -0,0 +1,237 @@ +#!/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: " buckit." + 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 "$@" diff --git a/install-linux.sh b/install-linux.sh new file mode 100755 index 000000000..3577271ef --- /dev/null +++ b/install-linux.sh @@ -0,0 +1,236 @@ +#!/bin/sh +# install-linux.sh — Linux native-package installer helper for buckit. +# +# Detects this host's package manager (dnf/yum/zypper, apt/apt-get/dpkg, or +# apk), downloads the matching .rpm/.deb/.apk for the latest stable release to +# a predictable filename (buckit.rpm / buckit.deb / buckit.apk), verifies its +# published SHA-256 checksum, and prints the command to install it. It does +# NOT run the package manager for you — the final install (which needs root) +# is left to you to review and run. +# +# Usage: +# curl -fsSL https://buckit-io.github.io/buckit/install-linux.sh | sh +# sudo dnf install ./buckit.rpm # or apt/apk, per the printed command +# +# 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 package 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}" + +err() { + echo "install-linux.sh: $*" >&2 + exit 1 +} + +info() { + echo "==> $*" +} + +# detect_sudo sets SUDO to the privilege-escalation prefix for install +# commands: empty when already root, "sudo " when sudo is available, otherwise +# empty with a warning (the printed command must then be run as root). +detect_sudo() { + if [ "$(id -u)" -eq 0 ]; then + SUDO="" + elif command -v sudo >/dev/null 2>&1; then + SUDO="sudo " + else + SUDO="" + info "not running as root and sudo not found — run the install command below as root" + fi +} + +# detect_arch sets ARCH to the Go-style arch tuple (amd64 / arm64) and +# requires a Linux host — native packages are Linux-only. +detect_arch() { + os="$(uname -s)" + [ "$os" = "Linux" ] || err "this installer is for Linux (detected '$os'). For a standalone binary use install-binary.sh; on Windows use install-windows.ps1" + + arch="$(uname -m)" + case "$arch" in + x86_64 | amd64) ARCH="amd64" ;; + arm64 | aarch64) ARCH="arm64" ;; + *) err "unsupported architecture '$arch'. Supported: amd64, arm64." ;; + esac +} + +# detect_pkg sets PKG (rpm|deb|apk) and INSTALL_CMD (the command prefix used +# to install a local package file) based on the available package manager. +# Requires SUDO to be set first (see detect_sudo). +detect_pkg() { + if command -v dnf >/dev/null 2>&1; then + PKG="rpm" + INSTALL_CMD="${SUDO}dnf install" + elif command -v yum >/dev/null 2>&1; then + PKG="rpm" + INSTALL_CMD="${SUDO}yum install" + elif command -v zypper >/dev/null 2>&1; then + PKG="rpm" + INSTALL_CMD="${SUDO}zypper install" + elif command -v apt >/dev/null 2>&1; then + PKG="deb" + INSTALL_CMD="${SUDO}apt install" + elif command -v apt-get >/dev/null 2>&1; then + PKG="deb" + INSTALL_CMD="${SUDO}apt-get install" + elif command -v apk >/dev/null 2>&1; then + PKG="apk" + INSTALL_CMD="${SUDO}apk add --allow-untrusted" + elif command -v rpm >/dev/null 2>&1; then + PKG="rpm" + INSTALL_CMD="${SUDO}rpm -i" + elif command -v dpkg >/dev/null 2>&1; then + PKG="deb" + INSTALL_CMD="${SUDO}dpkg -i" + else + err "no supported package manager found (need dnf/yum/zypper, apt/dpkg, or apk)" + fi +} + +# 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 -> hex digest on stdout +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + err "need sha256sum or shasum to verify the download" + fi +} + +# resolve_tag echoes the release tag to install, either the pinned +# BUCKIT_VERSION or the latest stable tag from the gh-pages pointer. +resolve_tag() { + if [ -n "${BUCKIT_VERSION:-}" ]; then + echo "$BUCKIT_VERSION" + return + fi + + pointer_url="$PAGES_BASE/server/buckit/release/linux-$ARCH/buckit.sha256sum" + # Pointer format: " buckit." + pointer="$(fetch "$pointer_url")" || err "could not fetch release pointer at $pointer_url" + name="$(echo "$pointer" | awk '{print $2}')" + tag="${name#buckit.}" + case "$tag" in + RELEASE.*) ;; + *) err "unexpected release pointer payload: $pointer" ;; + esac + echo "$tag" +} + +# pkg_version TAG -> nfpm package version, mirroring the release workflow: +# RELEASE.2026-05-11T17-20-40Z[.rcN] -> 20260511172040.0.0 +pkg_version() { + echo "$1" | sed 's/^RELEASE\.//' | sed 's/\.rc[0-9]*$//' | tr -d '\-:TZ' | sed 's/$/.0.0/' +} + +main() { + detect_arch + detect_sudo + detect_pkg + info "platform: linux-$ARCH ($PKG)" + + tag="$(resolve_tag)" + [ -n "$tag" ] || err "could not resolve a release tag" + info "release: $tag" + + pkgver="$(pkg_version "$tag")" + + # Build the package filename. nfpm maps amd64->x86_64 / arm64->aarch64 + # for rpm and apk; deb keeps the Debian arch names. + case "$PKG" in + rpm) + case "$ARCH" in + amd64) pkgarch="x86_64" ;; + arm64) pkgarch="aarch64" ;; + esac + asset="buckit-${pkgver}.${pkgarch}.rpm" + ;; + deb) + asset="buckit_${pkgver}_${ARCH}.deb" + ;; + apk) + case "$ARCH" in + amd64) pkgarch="x86_64" ;; + arm64) pkgarch="aarch64" ;; + esac + asset="buckit_${pkgver}_${pkgarch}.apk" + ;; + esac + + download_url="$RELEASE_BASE/$tag/$asset" + + # Save to a predictable filename (buckit.rpm / buckit.deb / buckit.apk) so + # the install command is stable and copy-pasteable in docs. The "./" form + # also matters for apt, which treats a path without a slash as a package + # name rather than a local file. + dldir="${BUCKIT_DOWNLOAD_DIR:-.}" + mkdir -p "$dldir" + pkgfile="$dldir/buckit.$PKG" + + # Download to a temporary sibling and only move it into the predictable + # path after the checksum verifies. A failed or interrupted download then + # can never clobber an existing good package or leave a partial/unverified + # file at the path the printed install command references. + tmpfile="$(mktemp "$dldir/.buckit.$PKG.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" + want_sha="$(fetch "$download_url.sha256sum" | awk '{print $1}')" || + err "could not fetch checksum at $download_url.sha256sum" + [ -n "$want_sha" ] || err "release checksum is empty" + + 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" + + mv -f "$tmpfile" "$pkgfile" + trap - EXIT + + echo + echo "Downloaded and verified:" + echo " $pkgfile" + echo + echo "To install, run:" + echo " $INSTALL_CMD \"$pkgfile\"" + echo +} + +main "$@" diff --git a/install-mac.sh b/install-mac.sh new file mode 100755 index 000000000..2a19765c3 --- /dev/null +++ b/install-mac.sh @@ -0,0 +1,237 @@ +#!/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: " buckit." + 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 "$@" diff --git a/install-windows.ps1 b/install-windows.ps1 new file mode 100644 index 000000000..c39d87684 --- /dev/null +++ b/install-windows.ps1 @@ -0,0 +1,150 @@ +<# +.SYNOPSIS + Windows installer helper for buckit. + +.DESCRIPTION + Downloads the Windows (x86_64) buckit executable for the latest stable + release to a predictable filename (buckit.exe), verifies its published + SHA-256 checksum, and prints how to put it on your PATH. It does NOT install + it for you. + +.EXAMPLE + irm https://buckit-io.github.io/buckit/install-windows.ps1 | iex + +.NOTES + 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 executable into + (default: the current directory) +#> + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +function Get-EnvOrDefault($name, $default) { + $value = [Environment]::GetEnvironmentVariable($name) + if ([string]::IsNullOrEmpty($value)) { return $default } + return $value +} + +$PagesBase = Get-EnvOrDefault 'BUCKIT_PAGES_BASE' 'https://buckit-io.github.io/buckit' +$ReleaseBase = Get-EnvOrDefault 'BUCKIT_RELEASE_BASE' 'https://github.com/buckit-io/buckit/releases/download' + +function Fetch-String($url) { + return (Invoke-WebRequest -UseBasicParsing -Uri $url).Content +} + +# Require Windows. $IsWindows is an automatic variable on PowerShell Core 6+ +# (where this script may run cross-platform); it is undefined on Windows +# PowerShell 5.1, which only runs on Windows — so treat undefined as Windows. +if (($null -ne $IsWindows) -and (-not $IsWindows)) { + throw "install-windows.ps1 is for Windows. On Linux or macOS use install-binary.sh" +} + +# Resolve the release tag: pinned BUCKIT_VERSION or the latest stable tag from +# the gh-pages pointer (format: " buckit.exe."). +# 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. +function Normalize-Sha($value) { + $sha = "$value".Trim().ToLower() + if ($sha -notmatch '^[0-9a-f]{64}$') { + throw "install-windows.ps1: malformed sha256 digest: '$value'" + } + return $sha +} + +# Assert-Tag rejects anything that is not a plain release identifier, so a +# value like '../../evil' cannot reach the download URL as path traversal. +function Assert-Tag($value) { + if ($value -notmatch '^RELEASE\.[A-Za-z0-9._-]+$') { + throw "install-windows.ps1: unexpected release tag '$value'" + } +} + +# A pinned BUCKIT_VERSION leaves $pointerSha empty: the pointer only ever +# describes the latest release, so it cannot vouch for an arbitrary pin. +$pointerSha = '' +$tag = [Environment]::GetEnvironmentVariable('BUCKIT_VERSION') +if ([string]::IsNullOrEmpty($tag)) { + $pointerUrl = "$PagesBase/server/buckit/release/windows-amd64/buckit.sha256sum" + try { + $pointer = (Fetch-String $pointerUrl).Trim() + } catch { + throw "install-windows.ps1: could not fetch release pointer at $pointerUrl" + } + $fields = ($pointer -split '\r?\n')[0] -split '\s+' + $name = $fields[1] + if ($name -notlike 'buckit.exe.*') { + throw "install-windows.ps1: unexpected release pointer payload: $pointer" + } + $tag = $name -replace '^buckit\.exe\.', '' + $pointerSha = Normalize-Sha $fields[0] +} +Assert-Tag $tag +Write-Host "==> release: $tag" + +$asset = "buckit-windows-amd64.exe.$tag" +$downloadUrl = "$ReleaseBase/$tag/$asset" + +$dlDir = Get-EnvOrDefault 'BUCKIT_DOWNLOAD_DIR' (Get-Location).Path +New-Item -ItemType Directory -Force -Path $dlDir | Out-Null +$exeFile = Join-Path $dlDir 'buckit.exe' + +# Refuse to run when the destination is a directory. Move-Item would move the +# temp file inside it and the script would report success while leaving nothing +# runnable at the path it prints. +if (Test-Path -LiteralPath $exeFile -PathType Container) { + throw "install-windows.ps1: $exeFile 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 download cannot clobber an existing +# good executable or leave a partial/unverified file. +$tmpFile = Join-Path $dlDir ".buckit.$([System.IO.Path]::GetRandomFileName()).tmp" +try { + Write-Host "==> downloading $asset" + Invoke-WebRequest -UseBasicParsing -Uri $downloadUrl -OutFile $tmpFile + + Write-Host "==> fetching published checksum" + $releaseSha = Normalize-Sha (((Fetch-String "$downloadUrl.sha256sum").Trim() -split '\s+')[0]) + + # 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 ($pointerSha) { + if ($pointerSha -ne $releaseSha) { + throw "install-windows.ps1: published digests disagree (pages $pointerSha, release $releaseSha) - refusing to continue" + } + $wantSha = $pointerSha + Write-Host "==> sha256 cross-checked against the release pointer" + } else { + $wantSha = $releaseSha + } + + $gotSha = (Get-FileHash -Algorithm SHA256 -Path $tmpFile).Hash.ToLower() + if ($gotSha -ne $wantSha) { + throw "install-windows.ps1: checksum mismatch (expected $wantSha, got $gotSha) — refusing to continue" + } + Write-Host "==> sha256 verified" + + Move-Item -Force -Path $tmpFile -Destination $exeFile +} finally { + if (Test-Path $tmpFile) { Remove-Item -Force $tmpFile } +} + +Write-Host "" +Write-Host "Downloaded and verified:" +Write-Host " $exeFile" +Write-Host "" +Write-Host "Run it from here:" +Write-Host " & `"$exeFile`"" +Write-Host "" +Write-Host "(Add its folder to your PATH to call 'buckit' from anywhere.)" +Write-Host "" diff --git a/server/buckit/archives/index.html b/server/buckit/archives/index.html new file mode 100644 index 000000000..dcd71bfa6 --- /dev/null +++ b/server/buckit/archives/index.html @@ -0,0 +1,63 @@ + + + + + + Buckit Releases + + + +
+
+ Release Archive +

Buckit Releases

+

Stable Buckit releases published to GitHub. Open any release to inspect artifacts, checksums, and signed binaries, or jump back to the current release landing page.

+ +
+
+ +
+
+ + \ No newline at end of file diff --git a/server/buckit/index.html b/server/buckit/index.html new file mode 100644 index 000000000..f450a66ee --- /dev/null +++ b/server/buckit/index.html @@ -0,0 +1,12 @@ + +Index of /server/buckit/ + + +

Index of /server/buckit/


+../
+release/
+

diff --git a/server/buckit/release/darwin-arm64/buckit.sha256sum b/server/buckit/release/darwin-arm64/buckit.sha256sum new file mode 100644 index 000000000..e0ea69a24 --- /dev/null +++ b/server/buckit/release/darwin-arm64/buckit.sha256sum @@ -0,0 +1 @@ +f6cfe0241b9d96107ff36969edca6662df78c7af713c1d1a49d4a3b988031839 buckit.RELEASE.2026-08-08T00-55-02Z diff --git a/server/buckit/release/index.html b/server/buckit/release/index.html new file mode 100644 index 000000000..0402131d0 --- /dev/null +++ b/server/buckit/release/index.html @@ -0,0 +1,219 @@ + + + + + + Buckit RELEASE.2026-08-08T00-55-02Z + + + +
+
+ Latest Stable Release +

Buckit

+

Signed release binaries for the Buckit S3-compatible object storage server.

+ +
+
+ Release tag + RELEASE.2026-08-08T00-55-02Z +
+
+ Repository + buckit-io/buckit +
+
+
+ +
+

Install

+

One-line installers that download the right artifact for your platform, verify its SHA-256 checksum, and print the command to finish. They do not install for you — the privileged step is yours to review and run.

+
+ + + + + + + + + + + + + + + + + + + + + + +
PlatformCommand
Linuxrpm / deb / apkcurl -fsSL https://buckit-io.github.io/buckit/install-linux.sh | sh
Linuxstandalone binarycurl -fsSL https://buckit-io.github.io/buckit/install-linux-binary.sh | sh
macOSApple Siliconcurl -fsSL https://buckit-io.github.io/buckit/install-mac.sh | sh
Windowsx86_64irm https://buckit-io.github.io/buckit/install-windows.ps1 | iex
+
+
+ +
+

Downloads

+

Choose the binary for your OS and architecture. Each platform card includes the signed artifact, checksum, and minisign signature.

+
+ + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PlatformBinaryChecksumSignature
Linux x86_64Standard Linuxbuckit-linux-amd64.RELEASE.2026-08-08T00-55-02Z.sha256sum.minisig
Linux arm64ARM64 Linuxbuckit-linux-arm64.RELEASE.2026-08-08T00-55-02Z.sha256sum.minisig
macOS arm64Apple Siliconbuckit-darwin-arm64.RELEASE.2026-08-08T00-55-02Z.sha256sum.minisig
Windows x86_6464-bit Windowsbuckit-windows-amd64.exe.RELEASE.2026-08-08T00-55-02Z.sha256sum.minisig
+
+
+ +
+

Linux packages

+

Native packages for Debian/Ubuntu, RPM, and Alpine distributions.

+ +
+
+ + diff --git a/server/buckit/release/linux-amd64/buckit.sha256sum b/server/buckit/release/linux-amd64/buckit.sha256sum new file mode 100644 index 000000000..a08eee15a --- /dev/null +++ b/server/buckit/release/linux-amd64/buckit.sha256sum @@ -0,0 +1 @@ +349fab1d9654751b47bd69915111b4ccf99e79a36f5ed4fc77ca8aaee21d45db buckit.RELEASE.2026-08-08T00-55-02Z diff --git a/server/buckit/release/linux-arm64/buckit.sha256sum b/server/buckit/release/linux-arm64/buckit.sha256sum new file mode 100644 index 000000000..d72a44b88 --- /dev/null +++ b/server/buckit/release/linux-arm64/buckit.sha256sum @@ -0,0 +1 @@ +e1af0c4ca22e8a0ae13ba941fee599db8e0472c6d12f5767484878bd500bb422 buckit.RELEASE.2026-08-08T00-55-02Z diff --git a/server/buckit/release/windows-amd64/buckit.sha256sum b/server/buckit/release/windows-amd64/buckit.sha256sum new file mode 100644 index 000000000..c171c4e8e --- /dev/null +++ b/server/buckit/release/windows-amd64/buckit.sha256sum @@ -0,0 +1 @@ +1753f05537ad3c85a9ad70e7faaab81659fd85e5266f678f56322e1f515dd675 buckit.exe.RELEASE.2026-08-08T00-55-02Z diff --git a/server/index.html b/server/index.html new file mode 100644 index 000000000..d970b3e45 --- /dev/null +++ b/server/index.html @@ -0,0 +1,12 @@ + +Index of /server/ + + +

Index of /server/


+../
+buckit/
+