#!/usr/bin/env bash set -euo pipefail REPO="${BEAM_REPO:-polybase/payy}" INSTALL_DIR="${BEAM_INSTALL_DIR:-$HOME/.local/bin}" VERSION_INPUT="${1:-${BEAM_VERSION:-}}" log() { printf '%s\n' "$*" } fail() { printf 'beam installer error: %s\n' "$*" >&2 exit 1 } need_cmd() { command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1" } has_cmd() { command -v "$1" >/dev/null 2>&1 } to_lowercase() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' } need_sha256_tool() { has_cmd sha256sum || has_cmd shasum || has_cmd openssl || fail "missing required command: sha256sum, shasum, or openssl" } resolve_target() { local os arch os="$(uname -s)" arch="$(uname -m)" case "$os/$arch" in Linux/x86_64) printf 'x86_64-unknown-linux-gnu' ;; Darwin/x86_64) printf 'x86_64-apple-darwin' ;; Darwin/arm64|Darwin/aarch64) printf 'aarch64-apple-darwin' ;; *) fail "unsupported platform: ${os} ${arch}" ;; esac } normalize_tag() { local version="$1" if [[ -z "$version" ]]; then return 1 fi if [[ "$version" == beam-v* ]]; then printf '%s' "$version" else printf 'beam-v%s' "$version" fi } stable_tags_from_releases_json() { awk ' function stable_tag_name(candidate, version) { version = candidate sub(/^beam-v/, "", version) return version ~ /^[0-9]+(\.[0-9]+)*(\+[0-9A-Za-z.-]+)?$/ } function version_is_newer(candidate, current, candidate_parts, current_parts, candidate_count, current_count, part_index, max_count, candidate_part, current_part) { sub(/^beam-v/, "", candidate) sub(/^beam-v/, "", current) candidate_count = split(candidate, candidate_parts, ".") current_count = split(current, current_parts, ".") max_count = candidate_count > current_count ? candidate_count : current_count for (part_index = 1; part_index <= max_count; part_index++) { candidate_part = (part_index in candidate_parts) ? candidate_parts[part_index] : 0 current_part = (part_index in current_parts) ? current_parts[part_index] : 0 sub(/[^0-9].*$/, "", candidate_part) sub(/[^0-9].*$/, "", current_part) candidate_part += 0 current_part += 0 if (candidate_part > current_part) { return 1 } if (candidate_part < current_part) { return 0 } } return 0 } function insert_candidate(candidate, insert_index, previous) { if (candidate in seen) { return } seen[candidate] = 1 tag_count++ tags[tag_count] = candidate for (insert_index = tag_count; insert_index > 1 && version_is_newer(tags[insert_index], tags[insert_index - 1]); insert_index--) { previous = tags[insert_index - 1] tags[insert_index - 1] = tags[insert_index] tags[insert_index] = previous } } function consider_release(record, candidate) { if (record !~ /"draft"[[:space:]]*:[[:space:]]*false/) { return } if (record !~ /"prerelease"[[:space:]]*:[[:space:]]*false/) { return } if (!match(record, /"tag_name"[[:space:]]*:[[:space:]]*"beam-v[^"]*"/)) { return } candidate = substr(record, RSTART, RLENGTH) sub(/^.*"tag_name"[[:space:]]*:[[:space:]]*"/, "", candidate) sub(/"$/, "", candidate) if (!stable_tag_name(candidate)) { return } insert_candidate(candidate) } { json = json $0 } END { capture = 0 depth = 0 escape = 0 in_string = 0 record = "" for (char_index = 1; char_index <= length(json); char_index++) { ch = substr(json, char_index, 1) if (capture) { record = record ch } if (escape) { escape = 0 continue } if (ch == "\\") { if (in_string) { escape = 1 } continue } if (ch == "\"") { in_string = !in_string continue } if (in_string) { continue } if (ch == "{") { depth++ if (depth == 1) { capture = 1 record = "{" } continue } if (ch == "}") { if (depth == 1) { consider_release(record) capture = 0 record = "" } depth-- } } for (tag_index = 1; tag_index <= tag_count; tag_index++) { print tags[tag_index] } } ' } stable_tags() { local compact_page page page_json page=1 while :; do page_json="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases?per_page=100&page=${page}")" compact_page="${page_json//[[:space:]]/}" if [[ "$compact_page" == "[]" ]]; then break fi printf '%s\n' "$page_json" page=$((page + 1)) done | stable_tags_from_releases_json } release_asset_metadata_from_json() { local asset_name="$1" awk -v asset_name="$asset_name" ' function consider_object(record, name, url, digest) { if (record !~ /"browser_download_url"[[:space:]]*:[[:space:]]*"/) { return } if (record !~ /"digest"[[:space:]]*:[[:space:]]*"/) { return } if (!match(record, /"name"[[:space:]]*:[[:space:]]*"[^"]*"/)) { return } name = substr(record, RSTART, RLENGTH) sub(/^.*"name"[[:space:]]*:[[:space:]]*"/, "", name) sub(/"$/, "", name) if (name != asset_name) { return } if (!match(record, /"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*"/)) { return } url = substr(record, RSTART, RLENGTH) sub(/^.*"browser_download_url"[[:space:]]*:[[:space:]]*"/, "", url) sub(/"$/, "", url) match(record, /"digest"[[:space:]]*:[[:space:]]*"[^"]*"/) digest = substr(record, RSTART, RLENGTH) sub(/^.*"digest"[[:space:]]*:[[:space:]]*"/, "", digest) sub(/"$/, "", digest) print url "\t" digest exit } { json = json $0 } END { depth = 0 escape = 0 in_string = 0 for (char_index = 1; char_index <= length(json); char_index++) { ch = substr(json, char_index, 1) if (depth > 0) { for (record_index = 1; record_index <= depth; record_index++) { record[record_index] = record[record_index] ch } } if (escape) { escape = 0 continue } if (ch == "\\") { if (in_string) { escape = 1 } continue } if (ch == "\"") { in_string = !in_string continue } if (in_string) { continue } if (ch == "{") { depth++ record[depth] = "{" continue } if (ch == "}") { consider_object(record[depth]) delete record[depth] depth-- } } } ' } release_is_stable_from_json() { awk ' function stable_tag_name(candidate, version) { version = candidate sub(/^beam-v/, "", version) return version ~ /^[0-9]+(\.[0-9]+)*(\+[0-9A-Za-z.-]+)?$/ } { json = json $0 } END { if (json ~ /"draft"[[:space:]]*:[[:space:]]*false/ && json ~ /"prerelease"[[:space:]]*:[[:space:]]*false/) { if (!match(json, /"tag_name"[[:space:]]*:[[:space:]]*"beam-v[^"]*"/)) { exit 1 } candidate = substr(json, RSTART, RLENGTH) sub(/^.*"tag_name"[[:space:]]*:[[:space:]]*"/, "", candidate) sub(/"$/, "", candidate) if (stable_tag_name(candidate)) { exit 0 } } exit 1 } ' } release_asset_metadata() { local tag="$1" local asset_name="$2" local release_json release_json="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/tags/${tag}")" if ! printf '%s\n' "$release_json" | release_is_stable_from_json; then return 0 fi printf '%s\n' "$release_json" | release_asset_metadata_from_json "$asset_name" } digest_has_valid_sha256() { local digest="$1" [[ "$digest" =~ ^sha256:[[:xdigit:]]{64}$ ]] } latest_release_asset_metadata() { local asset_digest asset_metadata asset_name asset_url tag tags asset_name="$1" tags="$(stable_tags)" while IFS= read -r tag; do [[ -n "$tag" ]] || continue asset_metadata="$(release_asset_metadata "$tag" "$asset_name")" [[ -n "$asset_metadata" ]] || continue IFS=$'\t' read -r asset_url asset_digest <<< "$asset_metadata" [[ -n "$asset_url" && -n "$asset_digest" ]] || continue if ! digest_has_valid_sha256 "$asset_digest"; then continue fi printf '%s\t%s\t%s\n' "$tag" "$asset_url" "$asset_digest" return 0 done <<< "$tags" } sha256_file() { local target="$1" if has_cmd sha256sum; then sha256sum "$target" | cut -d' ' -f1 return fi if has_cmd shasum; then shasum -a 256 "$target" | cut -d' ' -f1 return fi openssl dgst -sha256 -r "$target" | cut -d' ' -f1 } release_sha256_from_digest() { local asset_name="$1" local digest="$2" local sha256 if ! digest_has_valid_sha256 "$digest"; then if [[ "$digest" != sha256:* ]]; then fail "unsupported release digest for ${asset_name}: ${digest}" fi fail "invalid SHA-256 digest for ${asset_name}: ${digest}" fi sha256="${digest#sha256:}" to_lowercase "$sha256" } verify_asset_sha256() { local target="$1" local asset_name="$2" local digest="$3" local actual expected expected="$(release_sha256_from_digest "$asset_name" "$digest")" actual="$(to_lowercase "$(sha256_file "$target")")" if [[ "$actual" != "$expected" ]]; then fail "checksum mismatch for ${asset_name} (expected ${expected}, got ${actual})" fi } ensure_install_dir() { mkdir -p "$INSTALL_DIR" } main() { need_cmd awk need_cmd curl need_cmd mktemp need_cmd chmod need_cmd mv need_cmd tr need_sha256_tool local asset_digest asset_metadata asset_name asset_url target tag temp_file version target="$(resolve_target)" asset_name="beam-${target}" if [[ -n "$VERSION_INPUT" ]]; then tag="$(normalize_tag "$VERSION_INPUT")" version="${tag#beam-v}" asset_metadata="$(release_asset_metadata "$tag" "$asset_name")" [[ -n "$asset_metadata" ]] || fail "could not resolve stable release metadata for ${asset_name} in ${tag}" IFS=$'\t' read -r asset_url asset_digest <<< "$asset_metadata" [[ -n "$asset_url" && -n "$asset_digest" ]] || fail "invalid stable release metadata for ${asset_name} in ${tag}" digest_has_valid_sha256 "$asset_digest" || fail "invalid stable release metadata for ${asset_name} in ${tag}" else asset_metadata="$(latest_release_asset_metadata "$asset_name")" [[ -n "$asset_metadata" ]] || fail "could not determine the latest complete beam release for ${asset_name}" IFS=$'\t' read -r tag asset_url asset_digest <<< "$asset_metadata" [[ -n "$tag" && -n "$asset_url" && -n "$asset_digest" ]] || fail "invalid release metadata for ${asset_name}" version="${tag#beam-v}" fi ensure_install_dir temp_file="$(mktemp "${TMPDIR:-/tmp}/beam.XXXXXX")" trap 'rm -f "$temp_file"' EXIT log "Downloading beam ${version} for ${target}..." curl -fsSL "$asset_url" -o "$temp_file" verify_asset_sha256 "$temp_file" "$asset_name" "$asset_digest" chmod 755 "$temp_file" mv "$temp_file" "${INSTALL_DIR}/beam" trap - EXIT log "Installed beam to ${INSTALL_DIR}/beam" if [[ ":$PATH:" != *":${INSTALL_DIR}:"* ]]; then log "Add ${INSTALL_DIR} to your PATH to run \`beam\` directly." fi log "Run \`beam --help\` to get started." } script_source="${BASH_SOURCE[0]:-}" if [[ -z "$script_source" || "$script_source" == "$0" ]]; then main "$@" fi