#!/usr/bin/env bash # crucible — bootstrap wrapper (POSIX/bash). Prefers a crucible already on PATH # (Nix devshell / installed); otherwise downloads the latest released binary # from Gitea into a per-version cache and execs it. # # CANONICAL SOURCE: sow-tools/wrappers/crucible.sh. Do not hand-edit copies in # consumer repos — they are kept in sync by sow-tools CI (see drift-check). # # Requires: bash, curl, and sha256sum (Linux) or shasum (macOS). # Env: CRUCIBLE_HOME (cache dir), CRUCIBLE_TOKEN (private releases), # CRUCIBLE_GITEA (default https://git.westgate.pw), # CRUCIBLE_REPO (default ShadowsOverWestgate/sow-tools). set -euo pipefail # 0. Prefer an existing crucible (Nix users, or already installed). if command -v crucible >/dev/null 2>&1; then exec crucible "$@" fi gitea="${CRUCIBLE_GITEA:-https://git.westgate.pw}" repo="${CRUCIBLE_REPO:-ShadowsOverWestgate/sow-tools}" # 1. --repo-local materializes into /.crucible instead of the user cache. repo_local=0 if [ "${1:-}" = "--repo-local" ]; then repo_local=1; shift; fi # 2. OS/arch -> asset name. os="$(uname -s)"; arch="$(uname -m)" case "$os" in Linux) os=linux ;; Darwin) os=darwin ;; *) echo "crucible: unsupported OS '$os' — install ffmpeg-free crucible manually" >&2; exit 1 ;; esac case "$arch" in x86_64|amd64) arch=amd64 ;; aarch64|arm64) arch=arm64 ;; *) echo "crucible: unsupported arch '$arch'" >&2; exit 1 ;; esac asset="crucible-${os}-${arch}" # 3. Auth: anonymous first, token fallback. token="${CRUCIBLE_TOKEN:-}" if [ -z "$token" ] && [ -f "$HOME/.config/crucible/token" ]; then token="$(cat "$HOME/.config/crucible/token")" fi fetch() { # fetch URL DEST ; tries anon then token if curl -fsSL "$1" -o "$2" 2>/dev/null; then return 0; fi if [ -n "$token" ]; then curl -fsSL -H "Authorization: token $token" "$1" -o "$2"; return $?; fi return 1 } fetch_stdout() { # prints body; anon then token if curl -fsSL "$1" 2>/dev/null; then return 0; fi if [ -n "$token" ]; then curl -fsSL -H "Authorization: token $token" "$1"; return $?; fi return 1 } # 4. Resolve the latest release tag (no jq dependency). api="${gitea}/api/v1/repos/${repo}/releases/latest" tag="$(fetch_stdout "$api" 2>/dev/null | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4 || true)" # 5. Cache dir. if [ "$repo_local" = 1 ]; then cache_root="$(cd "$(dirname "$0")" && pwd)/.crucible" else cache_root="${CRUCIBLE_HOME:-${XDG_CACHE_HOME:-$HOME/.cache}/crucible}" fi # 6. Offline fallback: if tag lookup failed, use the newest cached version. if [ -z "$tag" ]; then if [ -d "$cache_root" ]; then cached="$(ls -1 "$cache_root" 2>/dev/null | sort -V | tail -1 || true)" if [ -n "$cached" ] && [ -x "$cache_root/$cached/crucible" ]; then echo "crucible: offline — using cached $cached" >&2 exec "$cache_root/$cached/crucible" "$@" fi fi echo "crucible: cannot reach $gitea and no cached binary found." >&2 echo " set CRUCIBLE_TOKEN if releases are private, or check the network." >&2 exit 1 fi dest_dir="$cache_root/$tag" bin="$dest_dir/crucible" # 7. Download + verify on cache miss. if [ ! -x "$bin" ]; then mkdir -p "$dest_dir" base="${gitea}/${repo}/releases/download/${tag}" tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT echo "crucible: fetching ${asset} ${tag}..." >&2 fetch "${base}/${asset}" "$tmp/crucible" || { echo "crucible: download failed for ${asset}" >&2; exit 1; } if fetch "${base}/SHA256SUMS" "$tmp/SHA256SUMS"; then want="$(grep " ${asset}\$" "$tmp/SHA256SUMS" | awk '{print $1}')" if command -v sha256sum >/dev/null 2>&1; then got="$(sha256sum "$tmp/crucible" | awk '{print $1}')" else got="$(shasum -a 256 "$tmp/crucible" | awk '{print $1}')" fi if [ -n "$want" ] && [ "$want" != "$got" ]; then echo "crucible: checksum mismatch for ${asset} (want $want got $got)" >&2; exit 1 fi fi chmod +x "$tmp/crucible" mv "$tmp/crucible" "$bin" fi exec "$bin" "$@"