PromtGarageMac / scripts /verify-mac.sh
smlflg's picture
Initial public upload from Projekte/PromtGarageMac
4977db2 verified
Raw History Blame Contribute Delete
15.2 kB
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APP="$ROOT/dist/PromptGarage.app"
INSTALL_APP="/Applications/PromptGarage.app"
SOURCE_DB=""
INSTALL=false
LAUNCH=false
SOURCE_SNAPSHOT_BEFORE=""
SOURCE_SNAPSHOT_AFTER=""
INSTALL_STAGING_DIR=""
INSTALL_PREVIOUS_APP=""
INSTALL_REJECTED_APP=""
INSTALL_HAD_PREVIOUS=false
INSTALL_PREVIOUS_MOVED=false
INSTALL_NEW_IN_PLACE=false
INSTALL_TRANSACTION_ACTIVE=false
INSTALL_COMMITTED=false
CLEANUP_TRAP_ACTIVE=false
usage() {
cat <<'EOF'
Usage: ./scripts/verify-mac.sh [options]
Options:
--source-db PATH Expose a real database read-only to integration tests via
PROMPTGARAGE_SOURCE_DB and prove its files are unchanged.
--launch Launch the verified dist/PromptGarage.app and prove that a
new PromptGarage process remains alive.
--install Replace /Applications/PromptGarage.app, then launch and
verify it. This is the only mode that writes to /Applications.
-h, --help Show this help.
Without --launch or --install, no app is opened. Without --install, nothing is
written to /Applications. This script never writes to --source-db.
EOF
}
fail() {
printf 'verify-mac: %s\n' "$*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--source-db)
[[ $# -ge 2 ]] || fail "--source-db requires a path"
[[ -n "$2" ]] || fail "--source-db requires a non-empty path"
SOURCE_DB="$2"
shift 2
;;
--launch)
LAUNCH=true
shift
;;
--install)
INSTALL=true
LAUNCH=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
fail "unknown option: $1 (use --help)"
;;
esac
done
assert_supported_host() {
[[ "$(uname -s)" == "Darwin" ]] || fail "this verification must run on macOS"
[[ "$(uname -m)" == "arm64" ]] || fail "an Apple Silicon (arm64) Mac is required"
local macos_version macos_major xcode_version xcode_major swift_version
macos_version="$(sw_vers -productVersion)"
macos_major="${macos_version%%.*}"
[[ "$macos_major" =~ ^[0-9]+$ ]] || fail "could not parse macOS version: $macos_version"
(( macos_major >= 26 )) || fail "macOS 26 or newer is required; found $macos_version"
xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')"
xcode_major="${xcode_version%%.*}"
[[ "$xcode_major" =~ ^[0-9]+$ ]] || fail "could not parse Xcode version: $xcode_version"
(( xcode_major >= 26 )) || fail "Xcode 26 or newer is required; found $xcode_version"
swift_version="$(swift --version | sed -n '1s/.*Swift version \([0-9][0-9.]*\).*/\1/p')"
[[ "$swift_version" == 6.2* ]] || fail "Swift 6.2 is required; found ${swift_version:-unknown}"
}
assert_exact_arm64() {
local binary="$1"
local architectures
architectures="$(lipo -archs "$binary" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
[[ "$architectures" == "arm64" ]] || fail "expected exactly arm64, found: ${architectures:-no architecture}"
}
snapshot_source_database() {
local output="$1"
local suffix candidate digest
: > "$output"
for suffix in "" "-wal" "-shm" "-journal"; do
candidate="${SOURCE_DB}${suffix}"
if [[ -e "$candidate" ]]; then
if [[ ! -f "$candidate" ]]; then
printf 'source database family contains a non-file: %s\n' "$candidate" >&2
return 1
fi
digest="$(shasum -a 256 "$candidate" | awk '{ print $1 }')" || return 1
printf 'present\t%s\t%s\n' "$suffix" "$digest" >> "$output"
else
printf 'absent\t%s\n' "$suffix" >> "$output"
fi
done
}
ensure_cleanup_trap() {
if [[ "$CLEANUP_TRAP_ACTIVE" != true ]]; then
trap cleanup_on_exit EXIT
CLEANUP_TRAP_ACTIVE=true
fi
}
rollback_install_transaction() {
local rollback_ok=true
[[ "$INSTALL_TRANSACTION_ACTIVE" == true ]] || return 0
[[ "$INSTALL_COMMITTED" != true ]] || return 0
if [[ "$INSTALL_NEW_IN_PLACE" == true ]]; then
if [[ -e "$INSTALL_APP" || -L "$INSTALL_APP" ]]; then
if mv "$INSTALL_APP" "$INSTALL_REJECTED_APP"; then
INSTALL_NEW_IN_PLACE=false
else
printf 'verify-mac: automatic rollback could not move the replacement aside: %s\n' "$INSTALL_APP" >&2
rollback_ok=false
fi
else
printf 'verify-mac: automatic rollback could not find the installed replacement: %s\n' "$INSTALL_APP" >&2
rollback_ok=false
fi
fi
if [[ "$INSTALL_PREVIOUS_MOVED" == true ]]; then
if [[ -e "$INSTALL_APP" || -L "$INSTALL_APP" ]]; then
printf 'verify-mac: automatic rollback cannot restore over an occupied path: %s\n' "$INSTALL_APP" >&2
rollback_ok=false
elif mv "$INSTALL_PREVIOUS_APP" "$INSTALL_APP"; then
INSTALL_PREVIOUS_MOVED=false
printf 'verify-mac: previous app restored after verification failure\n' >&2
else
printf 'verify-mac: automatic rollback failed; previous app remains at: %s\n' "$INSTALL_PREVIOUS_APP" >&2
rollback_ok=false
fi
elif [[ "$INSTALL_HAD_PREVIOUS" != true && ( -e "$INSTALL_REJECTED_APP" || -L "$INSTALL_REJECTED_APP" ) ]]; then
printf 'verify-mac: replacement app removed after verification failure\n' >&2
fi
if [[ "$rollback_ok" == true ]]; then
if [[ -n "$INSTALL_STAGING_DIR" ]] && ! rm -rf -- "$INSTALL_STAGING_DIR"; then
printf 'verify-mac: rollback restored the install path, but could not clean transaction files: %s\n' "$INSTALL_STAGING_DIR" >&2
return 1
fi
INSTALL_TRANSACTION_ACTIVE=false
return 0
fi
printf 'verify-mac: install rollback incomplete; preserving transaction files at: %s\n' "$INSTALL_STAGING_DIR" >&2
return 1
}
cleanup_on_exit() {
local status=$?
trap - EXIT
if [[ -n "$SOURCE_SNAPSHOT_BEFORE" && -f "$SOURCE_SNAPSHOT_BEFORE" ]]; then
if ! snapshot_source_database "$SOURCE_SNAPSHOT_AFTER"; then
printf 'verify-mac: could not re-check source database after failure\n' >&2
status=1
elif ! cmp -s "$SOURCE_SNAPSHOT_BEFORE" "$SOURCE_SNAPSHOT_AFTER"; then
printf 'verify-mac: SOURCE DATABASE CHANGED; refusing a successful result\n' >&2
diff -u "$SOURCE_SNAPSHOT_BEFORE" "$SOURCE_SNAPSHOT_AFTER" >&2 || true
status=1
fi
fi
if (( status != 0 )); then
rollback_install_transaction || status=1
elif [[ "$INSTALL_TRANSACTION_ACTIVE" == true && "$INSTALL_COMMITTED" != true ]]; then
printf 'verify-mac: install transaction reached EXIT before commit; rolling back\n' >&2
rollback_install_transaction || status=1
status=1
fi
[[ -z "$SOURCE_SNAPSHOT_BEFORE" ]] || rm -f "$SOURCE_SNAPSHOT_BEFORE"
[[ -z "$SOURCE_SNAPSHOT_AFTER" ]] || rm -f "$SOURCE_SNAPSHOT_AFTER"
exit "$status"
}
finish_source_guard() {
[[ -n "$SOURCE_SNAPSHOT_BEFORE" ]] || return 0
snapshot_source_database "$SOURCE_SNAPSHOT_AFTER" || fail "could not re-check source database"
if ! cmp -s "$SOURCE_SNAPSHOT_BEFORE" "$SOURCE_SNAPSHOT_AFTER"; then
diff -u "$SOURCE_SNAPSHOT_BEFORE" "$SOURCE_SNAPSHOT_AFTER" >&2 || true
fail "source database or a SQLite sidecar changed during verification"
fi
rm -f "$SOURCE_SNAPSHOT_BEFORE" "$SOURCE_SNAPSHOT_AFTER"
SOURCE_SNAPSHOT_BEFORE=""
SOURCE_SNAPSHOT_AFTER=""
}
resolve_xcode_scheme() {
local listing schemes candidate
listing="$(xcodebuild -list 2>&1)" || fail "xcodebuild could not inspect this Swift package"
schemes="$(printf '%s\n' "$listing" | awk '
/^[[:space:]]*Schemes:[[:space:]]*$/ { inside = 1; next }
inside && /^[[:space:]]+[^[:space:]]/ {
line = $0
sub(/^[[:space:]]+/, "", line)
sub(/[[:space:]]+$/, "", line)
print line
next
}
inside && NF > 0 { exit }
')"
if [[ -n "${PROMPTGARAGE_XCODE_SCHEME:-}" ]]; then
if printf '%s\n' "$schemes" | grep -Fxq "$PROMPTGARAGE_XCODE_SCHEME"; then
printf '%s\n' "$PROMPTGARAGE_XCODE_SCHEME"
return 0
fi
fail "PROMPTGARAGE_XCODE_SCHEME is not available: $PROMPTGARAGE_XCODE_SCHEME"
fi
for candidate in PromptGarage-Package PromptGarage; do
if printf '%s\n' "$schemes" | grep -Fxq "$candidate"; then
printf '%s\n' "$candidate"
return 0
fi
done
printf 'Available Xcode schemes:\n%s\n' "${schemes:-<none>}" >&2
fail "no PromptGarage package test scheme found; set PROMPTGARAGE_XCODE_SCHEME explicitly"
}
install_verified_app() {
local install_parent staged_app
[[ -d /Applications ]] || fail "/Applications does not exist"
[[ -w /Applications ]] || fail "/Applications is not writable; install manually rather than rerunning with sudo"
install_parent="$(dirname "$INSTALL_APP")"
INSTALL_STAGING_DIR="$(mktemp -d "$install_parent/.PromptGarage.install.XXXXXX")" \
|| fail "could not create same-filesystem install staging directory"
ensure_cleanup_trap
INSTALL_TRANSACTION_ACTIVE=true
staged_app="$INSTALL_STAGING_DIR/PromptGarage.app"
INSTALL_PREVIOUS_APP="$INSTALL_STAGING_DIR/Previous-PromptGarage.app"
INSTALL_REJECTED_APP="$INSTALL_STAGING_DIR/Rejected-PromptGarage.app"
ditto "$APP" "$staged_app" || fail "could not stage the verified app"
assert_exact_arm64 "$staged_app/Contents/MacOS/PromptGarage"
codesign --verify --deep --strict --verbose=2 "$staged_app" \
|| fail "staged app failed code-signature verification; existing installation was not changed"
if [[ -e "$INSTALL_APP" || -L "$INSTALL_APP" ]]; then
mv "$INSTALL_APP" "$INSTALL_PREVIOUS_APP" \
|| fail "could not preserve the existing installation; it was not changed"
INSTALL_HAD_PREVIOUS=true
INSTALL_PREVIOUS_MOVED=true
fi
mv "$staged_app" "$INSTALL_APP" || fail "could not move the staged app into place"
INSTALL_NEW_IN_PLACE=true
assert_exact_arm64 "$INSTALL_APP/Contents/MacOS/PromptGarage"
codesign --verify --deep --strict --verbose=2 "$INSTALL_APP" \
|| fail "installed app failed final code-signature verification"
printf 'Installed app pending launch and source-family verification: %s\n' "$INSTALL_APP"
}
commit_install_transaction() {
[[ "$INSTALL_TRANSACTION_ACTIVE" == true ]] || return 0
if ! rm -rf -- "$INSTALL_STAGING_DIR"; then
fail "installation passed, but transaction cleanup failed: $INSTALL_STAGING_DIR"
fi
INSTALL_COMMITTED=true
INSTALL_TRANSACTION_ACTIVE=false
INSTALL_PREVIOUS_MOVED=false
INSTALL_NEW_IN_PLACE=false
printf 'Installed verified app: %s\n' "$INSTALL_APP"
}
launch_and_verify_new_process() {
local launch_app="$1"
local expected_executable="$launch_app/Contents/MacOS/PromptGarage"
local before_pids pid process_command launched_pid=""
before_pids=" $(pgrep -x PromptGarage 2>/dev/null | tr '\n' ' ' || true) "
open -n "$launch_app"
for _ in 1 2 3 4 5 6 7 8 9 10; do
for pid in $(pgrep -x PromptGarage 2>/dev/null || true); do
case "$before_pids" in
*" $pid "*) continue ;;
esac
process_command="$(ps -p "$pid" -o command= 2>/dev/null || true)"
case "$process_command" in
"$expected_executable"*) launched_pid="$pid" ;;
esac
[[ -z "$launched_pid" ]] || break
done
[[ -z "$launched_pid" ]] || break
sleep 1
done
[[ -n "$launched_pid" ]] || fail "open returned, but no new process for $expected_executable stayed alive"
sleep 2
ps -p "$launched_pid" >/dev/null 2>&1 || fail "PromptGarage process $launched_pid exited during launch verification"
printf 'Launch verified: PromptGarage PID %s\n' "$launched_pid"
}
for command_name in uname sw_vers xcodebuild swift python3 awk sed grep lipo codesign shasum cmp diff; do
require_command "$command_name"
done
if [[ "$INSTALL" == true ]]; then
for command_name in dirname ditto mktemp mv rm; do
require_command "$command_name"
done
fi
if [[ "$LAUNCH" == true ]]; then
for command_name in open pgrep ps tr sleep; do
require_command "$command_name"
done
fi
if [[ -n "$SOURCE_DB" ]]; then
for command_name in dirname basename mktemp rm; do
require_command "$command_name"
done
fi
assert_supported_host
if [[ -n "$SOURCE_DB" ]]; then
[[ ! -L "$SOURCE_DB" ]] || fail "--source-db must not be a symlink; pass the real database path so SQLite sidecars are guarded"
[[ -f "$SOURCE_DB" ]] || fail "source database not found: $SOURCE_DB"
[[ -r "$SOURCE_DB" ]] || fail "source database is not readable: $SOURCE_DB"
SOURCE_DB="$(cd "$(dirname "$SOURCE_DB")" && pwd -P)/$(basename "$SOURCE_DB")"
[[ ! -L "$SOURCE_DB" ]] || fail "canonical --source-db resolved to a symlink; pass the real database path"
SOURCE_SNAPSHOT_BEFORE="$(mktemp "${TMPDIR:-/tmp}/promptgarage-source-before.XXXXXX")" \
|| fail "could not create the source-before snapshot"
ensure_cleanup_trap
SOURCE_SNAPSHOT_AFTER="$(mktemp "${TMPDIR:-/tmp}/promptgarage-source-after.XXXXXX")" \
|| fail "could not create the source-after snapshot"
snapshot_source_database "$SOURCE_SNAPSHOT_BEFORE" || fail "could not fingerprint source database"
export PROMPTGARAGE_SOURCE_DB="$SOURCE_DB"
fi
cd "$ROOT"
if [[ -n "$SOURCE_DB" ]]; then
python3 verification/verify_contract.py --source-db "$SOURCE_DB"
else
python3 verification/verify_contract.py
fi
swift package resolve
swift test --arch arm64
XCODE_SCHEME="$(resolve_xcode_scheme)"
printf 'Running Xcode package tests with scheme: %s\n' "$XCODE_SCHEME"
xcodebuild \
-scheme "$XCODE_SCHEME" \
-destination 'platform=macOS,arch=arm64' \
-configuration Debug \
ARCHS=arm64 \
ONLY_ACTIVE_ARCH=YES \
test
"$ROOT/scripts/build-app.sh"
[[ -x "$APP/Contents/MacOS/PromptGarage" ]] || fail "packaged app executable missing"
assert_exact_arm64 "$APP/Contents/MacOS/PromptGarage"
codesign --verify --deep --strict --verbose=2 "$APP"
LAUNCH_APP="$APP"
if [[ "$INSTALL" == true ]]; then
install_verified_app
LAUNCH_APP="$INSTALL_APP"
fi
if [[ "$LAUNCH" == true ]]; then
launch_and_verify_new_process "$LAUNCH_APP"
fi
finish_source_guard
if [[ "$INSTALL" == true ]]; then
commit_install_transaction
fi
trap - EXIT
printf 'Mac automated verification passed.\n'
if [[ "$LAUNCH" == true ]]; then
printf 'Complete verification/MAC_UI_SMOKE_CHECKLIST.md before calling the app done.\n'
else
printf 'No app was launched. Use --launch or --install only when ready for the UI smoke test.\n'
fi