File size: 15,226 Bytes
4977db2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
#!/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