#!/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