cloverleaf-larry/scripts/make-manifest.sh

185 lines
7.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# make-manifest.sh — regenerate larry-anywhere's MANIFEST with authoritative
# sha256 hashes for every shipped file.
#
# WHY (v0.8.11 — manifest-hashing speedup, Clover #14):
# The auto-updater (larry.sh sync_from_manifest) used to re-download ALL ~48
# manifest files over authenticated HTTPS every relaunch, then `cmp` locally
# to find the few that changed and discard the rest — ~3 min for a 3-file
# update. By publishing each file's expected sha256 IN the MANIFEST, the
# client fetches MANIFEST ONCE, hashes its local copies, and downloads only
# the files whose hash differs (or are missing). N round-trips -> 1 + a local
# hash pass + N real downloads.
#
# For that to be correct, the published hashes MUST be authoritative: this
# script recomputes them at release time. The MANIFEST hash always wins —
# local != manifest => download. A stale/missing/malformed hash can therefore
# never SKIP a real update (the client falls back to downloading on any doubt;
# see sync_from_manifest's fail-safe matrix), but it CAN cause a needless
# re-download — so we keep the hashes correct by regenerating here.
#
# FORMAT it emits (one entry per line, after the comment header):
# <path><TAB><sha256>
# Comment (#...) and blank lines are preserved verbatim from the source list so
# the human-readable section structure survives. Only path lines gain a hash.
#
# USAGE:
# scripts/make-manifest.sh # rewrite ./MANIFEST in place
# scripts/make-manifest.sh --check # verify MANIFEST is up to date (CI/hook)
# # exit 0 = in sync, 1 = drift, 2 = error
# scripts/make-manifest.sh --stdout # print the regenerated MANIFEST, no write
#
# RELEASE DISCIPLINE — every release MUST regenerate MANIFEST so the hashes
# match the bytes being pushed. This is wired so it can't be forgotten:
# 1. A pre-commit hook (scripts/hooks/pre-commit) runs `--check` and BLOCKS a
# commit whose MANIFEST hashes drift from the working tree. Install with
# `scripts/make-manifest.sh --install-hook`.
# 2. The CHANGELOG release checklist references this script.
# Run from the bundle root (the script cd's there regardless of invocation dir).
set -o pipefail
# ── Resolve bundle root (parent of this scripts/ dir) ─────────────────────────
_self="${BASH_SOURCE[0]:-$0}"
_self_dir="$(cd "$(dirname "$_self")" 2>/dev/null && pwd)"
ROOT="$(cd "$_self_dir/.." 2>/dev/null && pwd)"
[ -n "$ROOT" ] || { echo "error: cannot resolve bundle root" >&2; exit 2; }
MANIFEST="$ROOT/MANIFEST"
# ── sha256 tool detection — same fallback chain larry.sh uses, kept in sync ──
# Priority: sha256sum, shasum -a 256, openssl dgst -sha256. We normalize each
# tool's differing output to a bare lowercase 64-hex string.
_SHA_TOOL=""
_detect_sha_tool() {
[ -n "$_SHA_TOOL" ] && return 0
if command -v sha256sum >/dev/null 2>&1; then _SHA_TOOL="sha256sum"; return 0; fi
if command -v shasum >/dev/null 2>&1; then _SHA_TOOL="shasum"; return 0; fi
if command -v openssl >/dev/null 2>&1; then _SHA_TOOL="openssl"; return 0; fi
return 1
}
# _sha256_of FILE — print the bare 64-hex sha256 of FILE, or empty on failure.
_sha256_of() {
local f="$1" out=""
case "$_SHA_TOOL" in
sha256sum) out="$(sha256sum "$f" 2>/dev/null)" ;;
shasum) out="$(shasum -a 256 "$f" 2>/dev/null)" ;;
openssl) out="$(openssl dgst -sha256 "$f" 2>/dev/null)" ;;
*) return 1 ;;
esac
# Extract the first 64-hex run from whatever shape the tool emitted:
# sha256sum/shasum -> "<hash> <file>"
# openssl -> "SHA2-256(<file>)= <hash>"
out="$(printf '%s' "$out" | tr -d '\r' | grep -oE '[0-9a-fA-F]{64}' | head -1 | tr 'A-F' 'a-f')"
[ -n "$out" ] || return 1
printf '%s' "$out"
}
# ── Generate the hashed MANIFEST text to stdout ──────────────────────────────
# Source of truth for WHICH files ship: the existing MANIFEST's path lines plus
# its comment/section structure. We re-read the current MANIFEST, preserve every
# comment/blank line verbatim, and (re)hash every path line. A path line is the
# first whitespace-delimited token; any pre-existing hash is discarded and
# recomputed (so re-running is idempotent and always authoritative).
_generate() {
_detect_sha_tool || { echo "error: no sha256 tool (sha256sum / shasum / openssl) found" >&2; return 2; }
[ -f "$MANIFEST" ] || { echo "error: $MANIFEST not found (need the path list to hash)" >&2; return 2; }
local line path hash rc=0
while IFS= read -r line || [ -n "$line" ]; do
# Strip any CR so a CRLF-tainted source MANIFEST doesn't poison output.
line="${line//$'\r'/}"
case "$line" in
''|'#'*)
# Preserve comments and blank lines verbatim.
printf '%s\n' "$line"
continue
;;
esac
# First whitespace-delimited token is the path; drop any existing hash.
path="${line%%[[:space:]]*}"
[ -z "$path" ] && { printf '%s\n' "$line"; continue; }
if [ ! -f "$ROOT/$path" ]; then
echo "error: listed file missing on disk: $path" >&2
rc=2
# Emit the path with NO hash. The client treats a hashless line as
# "can't verify -> download" (fail-safe), so a missing file degrades to
# the old behaviour for that one entry rather than publishing a wrong hash.
printf '%s\n' "$path"
continue
fi
hash="$(_sha256_of "$ROOT/$path")"
if [ -z "$hash" ]; then
echo "error: could not hash $path" >&2
rc=2
printf '%s\n' "$path"
continue
fi
printf '%s\t%s\n' "$path" "$hash"
done < "$MANIFEST"
return $rc
}
# ── Entry points ──────────────────────────────────────────────────────────────
_install_hook() {
local hook_src="$ROOT/scripts/hooks/pre-commit"
local git_dir
git_dir="$(cd "$ROOT" && git rev-parse --git-dir 2>/dev/null)" || {
echo "error: not a git repo at $ROOT" >&2; return 2; }
[ -f "$hook_src" ] || { echo "error: $hook_src missing" >&2; return 2; }
local dest="$git_dir/hooks/pre-commit"
cp "$hook_src" "$dest" && chmod +x "$dest" && echo "installed pre-commit hook -> $dest"
}
case "${1:-}" in
--stdout)
_generate
exit $?
;;
--check)
# Compare freshly generated text against the on-disk MANIFEST.
tmp="$(mktemp)" || { echo "error: mktemp failed" >&2; exit 2; }
if ! _generate > "$tmp"; then
rc=$?
rm -f "$tmp"
echo "make-manifest --check: generation error (rc=$rc)" >&2
exit "$rc"
fi
if cmp -s "$tmp" "$MANIFEST"; then
rm -f "$tmp"
echo "MANIFEST is up to date."
exit 0
fi
echo "MANIFEST is OUT OF DATE — run scripts/make-manifest.sh to regenerate." >&2
echo "drift (diff: current MANIFEST vs freshly hashed):" >&2
diff "$MANIFEST" "$tmp" >&2 || true
rm -f "$tmp"
exit 1
;;
--install-hook)
_install_hook
exit $?
;;
--help|-h)
sed -n '2,40p' "$0"
exit 0
;;
'')
# Default: rewrite MANIFEST in place (write to temp, then move atomically).
tmp="$(mktemp)" || { echo "error: mktemp failed" >&2; exit 2; }
if ! _generate > "$tmp"; then
rc=$?
rm -f "$tmp"
echo "make-manifest: generation failed (rc=$rc); MANIFEST left unchanged" >&2
exit "$rc"
fi
mv "$tmp" "$MANIFEST" && echo "MANIFEST regenerated with sha256 hashes ($(grep -cE ' [0-9a-f]{64}$' "$MANIFEST") entries hashed)."
exit 0
;;
*)
echo "error: unknown argument: $1 (try --help)" >&2
exit 2
;;
esac