Files
stack/scripts/sync-dev-extensions.sh
T

143 lines
4.6 KiB
Bash
Executable File

#!/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"