#!/usr/bin/env bash # The machine role: the one interview answer Panama keeps. # # What is pinned, and why each half matters: # # * The interview asks the role first and a server is never asked the # desktop's questions -- hardware and extras feed stages the server path # does not run, and a question nothing consumes is a control that lies. # * `--server` presets the answer without a prompt, because the fresh-VPS # path runs from a curl with nobody to interview twice. # * setup/lib/machine-role reads env over file over default, and records # only values it can read back -- an unreadable role file must degrade to # desktop, never to an error, because every pre-role machine has none. # * install runs different stages per role, and the server list must never # silently reacquire a desktop stage (or the reverse). set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" interview="$repo_dir/setup/scripts/interview" install_script="$repo_dir/install" machine_role="$repo_dir/setup/lib/machine-role" boot="$repo_dir/boot" findings=() note() { findings+=("$1"); } # ── The helper, sandboxed ──────────────────────────────────────────────────── work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT role_of() { env -u PANAMA_ROLE XDG_STATE_HOME="$work/state" ${1:+PANAMA_ROLE="$1"} \ bash -c "source '$machine_role'; panama_role" } [[ "$(role_of "")" == desktop ]] \ || note 'with no file and no env, the role is not desktop' env XDG_STATE_HOME="$work/state" \ bash -c "source '$machine_role'; panama_role_record server" [[ "$(role_of "")" == server ]] \ || note 'a recorded server role does not read back' [[ "$(role_of desktop)" == desktop ]] \ || note 'an exported PANAMA_ROLE does not win over the recorded file' printf 'gibberish\n' >"$work/state/panama/role" [[ "$(role_of "")" == desktop ]] \ || note 'a role file with an unknown value does not degrade to desktop' env XDG_STATE_HOME="$work/state" \ bash -c "source '$machine_role'; panama_role_record nonsense" [[ "$(role_of "")" == desktop ]] \ || note 'recording an unknown value does not degrade to desktop' # ── The interview, per role ────────────────────────────────────────────────── # # The same stub the interview contract stands up, plus `choose` so the role # question is answerable. GUM_STUB_CHOOSE also feeds the extras checklist on # a desktop run, which is why the desktop case leaves it empty. stub_dir="$work/bin" mkdir -p "$stub_dir" cat >"$stub_dir/gum" <<'STUB' #!/usr/bin/env bash case "$1" in input) printf '%s\n' "$GUM_STUB_INPUT" ;; confirm) [[ "$GUM_STUB_CONFIRM" == yes ]] ;; choose) printf '%s\n' "$GUM_STUB_CHOOSE" ;; style) shift; printf '%s\n' "${@: -1}" ;; *) exit 0 ;; esac STUB chmod +x "$stub_dir/gum" answers="$work/answers" ask_interview() { : >"$answers" GUM_STUB_INPUT="x" GUM_STUB_CONFIRM=yes GUM_STUB_CHOOSE="${2:-}" \ PANAMA_ANSWERS="$answers" PANAMA_ROLE_PRESET="${1:-}" \ PATH="$stub_dir:$PATH" bash "$interview" >/dev/null 2>&1 } answered() { ( # shellcheck source=/dev/null source "$answers"; printf '%s' "${!1:-}" ) } ask_interview "" server \ || note 'the interview fails when the role question answers server' [[ "$(answered PANAMA_ROLE)" == server ]] \ || note 'choosing server does not record PANAMA_ROLE=server' [[ "$(answered PANAMA_EXTRAS)" == "" ]] \ || note 'a server was asked the extras question' [[ "$(answered PANAMA_NVIDIA)" == no ]] \ || note 'a server run does not record the hardware defaults' ask_interview "" "" \ || note 'the interview fails when the role question is escaped' [[ "$(answered PANAMA_ROLE)" == desktop ]] \ || note 'an escaped role question does not default to desktop' ask_interview server desktop \ || note 'the interview fails under a --server preset' [[ "$(answered PANAMA_ROLE)" == server ]] \ || note 'PANAMA_ROLE_PRESET=server does not win: the prompt was asked anyway' # ── install runs the right stages ──────────────────────────────────────────── # # Read from the source rather than run: the stage lists are data, and what # matters is which names each role's list carries. # Only the two literal lists: the upgrade filter reassigns STAGES from a # variable, which is not a role's stage list. server_stages="$(sed -n 's/^ STAGES=(\(.*\))$/\1/p' "$install_script" | grep '^install-packages' | head -1)" desktop_stages="$(sed -n 's/^ STAGES=(\(.*\))$/\1/p' "$install_script" | grep '^install-packages' | tail -1)" for stage in install-packages link-dotfiles link-user setup-server link-server setup-identity; do grep -qw "$stage" <<<"$server_stages" \ || note "the server stage list is missing $stage" done for stage in install-hardware change-settings link-skills link-vicinae-scripts; do grep -qw "$stage" <<<"$server_stages" \ && note "the server stage list acquired the desktop stage $stage" done for stage in install-packages link-dotfiles link-skills link-user change-settings link-vicinae-scripts setup-identity install-hardware; do grep -qw "$stage" <<<"$desktop_stages" \ || note "the desktop stage list lost $stage" done grep -q -- '--server)' "$install_script" \ || note 'install does not accept --server' grep -q 'panama_role_record' "$install_script" \ || note 'install never records the role, so panama update cannot know it' # ── boot's front door ──────────────────────────────────────────────────────── grep -q -- '--server) SERVER=1' "$boot" \ || note 'boot does not accept --server' grep -q 'not root' "$boot" \ || note 'boot no longer refuses a root run without --server' grep -q 'runuser' "$boot" \ || note 'the root bootstrap does not hand off to the created user' # ── Report ─────────────────────────────────────────────────────────────────── if (( ${#findings[@]} > 0 )); then mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u) printf 'role contract: %d finding(s)\n' "${#findings[@]}" >&2 printf ' - %s\n' "${findings[@]}" >&2 exit 1 fi printf 'role contract: PASS\n'