#!/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): # # 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 -> " " # openssl -> "SHA2-256()= " 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