fdec887beb
Add scripts/macos-install-test.sh, a throwaway-user harness for exercising install.sh the way a brand-new user would on macOS, plus the research note that motivates the approach. The harness has up/run/status/down/deep-reset subcommands. Because install.sh writes only to the user home (pipx venv, ~/.bot-bottle, a PATH line) and never installs the backend, deleting the account is a complete, deterministic reset of the install surface. A disposable macOS VM can't stand in on M1/M2: the Apple `container` backend needs Virtualization.framework, and running it inside a guest VM requires nested virtualization (M3+ only), so a throwaway user is the only way to reach the real host backend from a clean $HOME. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
196 lines
8.2 KiB
Bash
Executable File
196 lines
8.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Clean-install test harness for the macOS (Apple `container`) path.
|
|
#
|
|
# Exercises install.sh the way a brand-new user would, inside a throwaway
|
|
# macOS account you create and delete from the CLI. install.sh's entire
|
|
# footprint is user-home-local — the pipx venv under ~/.local (or the pip
|
|
# --user tree under ~/Library/Python), the ~/.bot-bottle config dir, and a
|
|
# PATH line in the login shell. It never installs the backend itself (see
|
|
# the header of install.sh), so deleting the user is a complete,
|
|
# deterministic reset of everything the installer touched. The Apple
|
|
# `container` runtime is a HOST prerequisite installed once and kept;
|
|
# `deep-reset` is the rare escape hatch that also removes it.
|
|
#
|
|
# Why a throwaway user and not a disposable VM: bot-bottle's default macOS
|
|
# backend is Apple `container`, which runs each container in its own
|
|
# Virtualization.framework microVM. Running that backend inside a macOS
|
|
# guest VM needs nested virtualization, which Apple gates to M3+ silicon.
|
|
# On M1/M2 a separate user account is the only way to get a clean $HOME
|
|
# while still reaching the real host backend. Full rationale in
|
|
# docs/research/testing-clean-install-on-macos.md.
|
|
#
|
|
# Usage:
|
|
# sudo ./scripts/macos-install-test.sh up # create the throwaway user
|
|
# sudo ./scripts/macos-install-test.sh run # run install.sh (+doctor) as it
|
|
# ./scripts/macos-install-test.sh status # user present? backend ready?
|
|
# sudo ./scripts/macos-install-test.sh down # delete user + home (the reset)
|
|
# sudo ./scripts/macos-install-test.sh deep-reset # ALSO uninstall host `container`
|
|
#
|
|
# A full clean cycle is: sudo ... up && sudo ... run && sudo ... down
|
|
#
|
|
# Config via env:
|
|
# BB_TEST_USER account short name (default: bbtest)
|
|
# BB_TEST_FULLNAME account full name (default: "bot-bottle install test")
|
|
# BB_TEST_ADMIN 1=admin (reach container svc), 0=standard (default: 1)
|
|
# BB_TEST_INSTALL_URL curl this install.sh instead of piping the local checkout
|
|
# BOT_BOTTLE_INSTALL_SPEC passed through to install.sh (pip / git spec)
|
|
#
|
|
# Notes:
|
|
# * Run from a normally-booted admin session. Grant Terminal *Full Disk
|
|
# Access* (System Settings -> Privacy & Security) or `down` half-fails
|
|
# with error -14120 and leaves an orphaned account.
|
|
# * `sysadminctl` always exits 0 even on failure, so `up`/`down` verify
|
|
# the result with `dscl` and fail loudly on a mismatch.
|
|
# * The account is created without a password: `run` drives it headlessly
|
|
# via `sudo -u`, which never needs the target's password. The account
|
|
# cannot GUI-login, which this harness does not require.
|
|
# * `run` covers the installer + `bot-bottle doctor`. Actually launching a
|
|
# bottle from the throwaway user may need a full launchd user session
|
|
# (`launchctl asuser`); on M1/M2 the backend can't run under nested virt
|
|
# anyway, so this harness stops at install + doctor.
|
|
|
|
set -euo pipefail
|
|
|
|
USER_NAME="${BB_TEST_USER:-bbtest}"
|
|
FULL_NAME="${BB_TEST_FULLNAME:-bot-bottle install test}"
|
|
ADMIN="${BB_TEST_ADMIN:-1}"
|
|
|
|
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
_REPO_ROOT="$(cd "$_SCRIPT_DIR/.." && pwd)"
|
|
|
|
# --- guards ----------------------------------------------------------
|
|
require_macos() {
|
|
[ "$(uname -s)" = "Darwin" ] \
|
|
|| { echo "error: this harness is macOS-only (uname is $(uname -s))" >&2; exit 1; }
|
|
}
|
|
|
|
require_root() {
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
echo "error: '$1' needs root; re-run under sudo" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
user_exists() { dscl . -read "/Users/$USER_NAME" >/dev/null 2>&1; }
|
|
|
|
# Run a shell snippet as the throwaway user in a fresh login shell.
|
|
run_as_user() { sudo -u "$USER_NAME" -i sh -c "$1"; }
|
|
|
|
# --- commands --------------------------------------------------------
|
|
cmd_up() {
|
|
require_macos
|
|
require_root up
|
|
if user_exists; then
|
|
echo "$USER_NAME already exists; nothing to do (run 'down' first to reset)"
|
|
return 0
|
|
fi
|
|
local admin_flag=()
|
|
[ "$ADMIN" = "1" ] && admin_flag=(-admin)
|
|
# No -password: the account is only ever driven headlessly via `sudo -u`,
|
|
# which doesn't need one. sysadminctl warns about FileVault here; that's
|
|
# irrelevant to a headless test account.
|
|
sysadminctl -addUser "$USER_NAME" -fullName "$FULL_NAME" "${admin_flag[@]}" || true
|
|
# sysadminctl exits 0 regardless of outcome, so confirm the account landed.
|
|
user_exists || { echo "error: failed to create $USER_NAME" >&2; exit 1; }
|
|
echo "created $USER_NAME (admin=$ADMIN). Install into it with: sudo $0 run"
|
|
}
|
|
|
|
cmd_run() {
|
|
require_macos
|
|
require_root run
|
|
user_exists || { echo "error: $USER_NAME does not exist; run 'sudo $0 up' first" >&2; exit 1; }
|
|
|
|
local spec_env=""
|
|
[ -n "${BOT_BOTTLE_INSTALL_SPEC:-}" ] \
|
|
&& spec_env="BOT_BOTTLE_INSTALL_SPEC='$BOT_BOTTLE_INSTALL_SPEC' "
|
|
|
|
echo "== installing bot-bottle as $USER_NAME =="
|
|
if [ -n "${BB_TEST_INSTALL_URL:-}" ]; then
|
|
run_as_user "curl -fsSL '$BB_TEST_INSTALL_URL' | ${spec_env}sh"
|
|
else
|
|
# Test THIS checkout's install.sh, not the published one, so a PR is
|
|
# verifiable before it lands. Stage it world-readable in /tmp because
|
|
# the throwaway user can't read the tester's (mode 700) home.
|
|
local staged
|
|
staged="$(mktemp /tmp/bb-install.XXXXXX.sh)"
|
|
cp "$_REPO_ROOT/install.sh" "$staged"
|
|
chmod 644 "$staged"
|
|
run_as_user "${spec_env}sh '$staged'" || { rm -f "$staged"; exit 1; }
|
|
rm -f "$staged"
|
|
fi
|
|
echo "== install.sh runs 'doctor' itself; re-check anytime with: $0 status =="
|
|
}
|
|
|
|
cmd_status() {
|
|
require_macos
|
|
if user_exists; then
|
|
echo "user: $USER_NAME present"
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
echo "doctor (as $USER_NAME):"
|
|
run_as_user 'command -v bot-bottle >/dev/null 2>&1 \
|
|
&& bot-bottle doctor \
|
|
|| echo " bot-bottle not installed for this user yet"' || true
|
|
else
|
|
echo " (re-run under sudo to run 'bot-bottle doctor' as $USER_NAME)"
|
|
fi
|
|
else
|
|
echo "user: $USER_NAME absent"
|
|
fi
|
|
if command -v container >/dev/null 2>&1; then
|
|
echo "backend: apple 'container' present ($(container --version 2>/dev/null | head -1))"
|
|
else
|
|
echo "backend: apple 'container' NOT on PATH (host prerequisite; install once)"
|
|
fi
|
|
}
|
|
|
|
cmd_down() {
|
|
require_macos
|
|
require_root down
|
|
if ! user_exists; then
|
|
echo "$USER_NAME not present; nothing to remove"
|
|
return 0
|
|
fi
|
|
# A plain -deleteUser removes the home dir, which is the whole reset.
|
|
# -secure is a no-op on modern macOS (secure erase of the home folder
|
|
# was removed in Sierra), so it buys nothing here.
|
|
sysadminctl -deleteUser "$USER_NAME" || true
|
|
if user_exists; then
|
|
echo "error: $USER_NAME still present after delete." >&2
|
|
echo " - grant Terminal Full Disk Access (System Settings > Privacy & Security), or" >&2
|
|
echo " - it may hold the last Secure Token (won't happen while another admin exists)" >&2
|
|
exit 1
|
|
fi
|
|
echo "removed $USER_NAME and its home — install surface is clean."
|
|
}
|
|
|
|
cmd_deep_reset() {
|
|
require_macos
|
|
require_root deep-reset
|
|
# Remove the user first (idempotent), then the HOST-level container
|
|
# runtime that a user deletion leaves behind under /usr/local + launchd.
|
|
cmd_down || true
|
|
if command -v container >/dev/null 2>&1; then
|
|
# The service can run in more than one launchd context (the invoking
|
|
# user's and root's), so stop both, best-effort.
|
|
[ -n "${SUDO_USER:-}" ] && sudo -u "$SUDO_USER" container system stop 2>/dev/null || true
|
|
container system stop 2>/dev/null || true
|
|
if [ -x /usr/local/bin/uninstall-container.sh ]; then
|
|
/usr/local/bin/uninstall-container.sh -d || true
|
|
echo "uninstalled the host Apple 'container' runtime"
|
|
else
|
|
echo "note: /usr/local/bin/uninstall-container.sh not found; runtime left as-is" >&2
|
|
fi
|
|
else
|
|
echo "no 'container' runtime on PATH; nothing further to remove"
|
|
fi
|
|
}
|
|
|
|
case "${1:-}" in
|
|
up) cmd_up ;;
|
|
run) cmd_run ;;
|
|
status) cmd_status ;;
|
|
down) cmd_down ;;
|
|
deep-reset) cmd_deep_reset ;;
|
|
*) echo "usage: $0 {up|run|status|down|deep-reset}" >&2 ; exit 2 ;;
|
|
esac
|