Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user