143 lines
4.6 KiB
Bash
Executable File
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"
|