#!/usr/bin/env bash # Install canonical extension source into the project-local native Pi directory. set -euo pipefail cd "$(dirname "$0")/.." MODE="sync" case "${1:-}" in "") ;; --check) MODE="check" ;; *) echo "usage: scripts/sync-dev-extensions.sh [--check]" >&2; exit 4 ;; esac SOURCE_ROOT="$PWD/extensions" PI_ROOT="$PWD/.pi" DEST="$PI_ROOT/extensions" RECORD="$PI_ROOT/extensions.installed.sha256" LOCK="$PI_ROOT/.extensions-sync.lock" TRANSACTION="$PI_ROOT/.extensions-transaction" BACKUP="$PI_ROOT/.extensions-backup" MANAGED=(goal mosaic-core) mkdir -p "$PI_ROOT" command -v flock >/dev/null || { echo "sync-dev-extensions: flock is required" >&2; exit 2; } exec 9>"$LOCK" if ! flock -n 9; then echo "sync-dev-extensions: another sync is active" >&2 exit 3 fi manifest_tree() { local root="$1" output="$2" if find "$root" -type l -print -quit | grep -q .; then echo "sync-dev-extensions: symlink found in $root" >&2 return 2 fi ( cd "$root" while IFS= read -r -d '' path; do path="${path#./}" if [ -d "$path" ]; then printf 'd %q\n' "$path" elif [ -f "$path" ]; then printf 'f %s %q\n' "$(sha256sum "$path" | cut -d ' ' -f 1)" "$path" else echo "sync-dev-extensions: unsupported entry $path" >&2 exit 2 fi done < <(find . -mindepth 1 -print0 | LC_ALL=C sort -z) ) > "$output" } # Recover a process killed after transaction intent was recorded. A matching # destination and installed manifest commits; every other state rolls back. recover_transaction() { [ -f "$TRANSACTION" ] || return 0 local current="$PI_ROOT/.recovery-manifest.$$" local destination_matches=false if [ -d "$DEST" ] && [ -f "$RECORD" ] && manifest_tree "$DEST" "$current" && cmp -s "$RECORD" "$current"; then destination_matches=true fi rm -f "$current" if [ -d "$BACKUP" ]; then if [ "$destination_matches" = "true" ]; then rm -rf "$BACKUP" else rm -rf "$DEST" mv "$BACKUP" "$DEST" fi elif [ "$destination_matches" = "false" ]; then rm -rf "$DEST" fi rm -f "$TRANSACTION" } recover_transaction find "$PI_ROOT" -mindepth 1 -maxdepth 1 -type d -name '.extensions-sync.*' -exec rm -rf {} + WORK="$(mktemp -d "$PI_ROOT/.extensions-sync.XXXXXX")" COMMITTED=false cleanup() { local rc=$? if [ "$COMMITTED" = "false" ] && [ -f "$TRANSACTION" ]; then recover_transaction || true; fi rm -rf "$WORK" exit "$rc" } trap cleanup EXIT for name in "${MANAGED[@]}"; do [ -d "$SOURCE_ROOT/$name" ] || { echo "sync-dev-extensions: missing source $SOURCE_ROOT/$name" >&2; exit 2; } done if find "$SOURCE_ROOT" -type l -print -quit | grep -q .; then echo "sync-dev-extensions: source symlinks are forbidden" >&2 exit 2 fi mapfile -t ENTRYPOINTS < <(find "$SOURCE_ROOT" -name index.ts -type f -printf '%P\n' | LC_ALL=C sort) if [ "${#ENTRYPOINTS[@]}" -ne 1 ] || [ "${ENTRYPOINTS[0]}" != "goal/index.ts" ]; then echo "sync-dev-extensions: expected exactly one entrypoint, goal/index.ts" >&2 exit 2 fi EXPECTED="$WORK/expected" mkdir "$EXPECTED" for name in "${MANAGED[@]}"; do cp -a "$SOURCE_ROOT/$name" "$EXPECTED/$name"; done EXPECTED_MANIFEST="$WORK/expected.sha256" manifest_tree "$EXPECTED" "$EXPECTED_MANIFEST" if [ "$MODE" = "check" ]; then [ -d "$DEST" ] || { echo "sync-dev-extensions: development installation is missing" >&2; exit 1; } CURRENT="$WORK/current.sha256" manifest_tree "$DEST" "$CURRENT" || exit 1 if ! cmp -s "$EXPECTED_MANIFEST" "$CURRENT"; then echo "sync-dev-extensions: development installation differs from canonical source" >&2 exit 1 fi echo "sync-dev-extensions: canonical source matches .pi installation" COMMITTED=true exit 0 fi if [ -d "$DEST" ]; then CURRENT="$WORK/current.sha256" manifest_tree "$DEST" "$CURRENT" || exit 1 if [ -f "$RECORD" ]; then cmp -s "$RECORD" "$CURRENT" || { echo "sync-dev-extensions: refusing to overwrite local changes under .pi/extensions" >&2; exit 1; } else cmp -s "$EXPECTED_MANIFEST" "$CURRENT" || { echo "sync-dev-extensions: unrecorded .pi/extensions differs from canonical source" >&2; exit 1; } fi fi [ ! -e "$BACKUP" ] || { echo "sync-dev-extensions: unreconciled backup remains" >&2; exit 2; } printf 'prepared\n' > "$TRANSACTION" if [ -e "$DEST" ]; then mv "$DEST" "$BACKUP"; fi mv "$EXPECTED" "$DEST" if [ "${MOSAIC_SYNC_TEST_FAULT:-}" = "after-destination" ]; then kill -KILL "$$" fi RECORD_TMP="$WORK/installed.sha256" cp "$EXPECTED_MANIFEST" "$RECORD_TMP" mv "$RECORD_TMP" "$RECORD" COMMITTED=true rm -rf "$BACKUP" rm -f "$TRANSACTION" echo "sync-dev-extensions: installed canonical goal extension into .pi/extensions"