#!/usr/bin/env bash # Everything Panama needs to be told, asked before anything is installed. # # sunhat's failure mode was a question -- or a failure -- twenty minutes into a # run, with a person needed at the keyboard to get past it. Walking away from an # install meant coming back to a prompt that had been waiting an hour. # # So Panama asks first and then runs untouched. Everything interactive lives # here, at the front, where the answers are cheap to change and nothing has been # installed yet. # # Answers are NOT remembered between runs. There is no state file to go stale, and # nothing personal is committed, which is what keeps this repository something # somebody else could clone. Re-answering a handful of questions costs less than # maintaining an answers file that drifts out of date. # # This asks only what a stage in this repository actually consumes. A prompt # whose answer nothing reads is a control that lies. # # The hardware questions name what was found rather than asking a person to # recite their own machine, and they are not asked at all on a machine they # would do nothing to. Detection alone would be worse: it would remove the # ability to decline a proprietary driver on a machine that has the card. set -uo pipefail # install passes the path. Refusing to guess one keeps the answers where the # caller can delete them, rather than somewhere this script invented. answers="${PANAMA_ANSWERS:-}" [[ -n "$answers" ]] || { printf 'interview: PANAMA_ANSWERS is not set; run this through ./install\n' >&2; exit 1; } : >"$answers" # %q so a value containing a space, a quote or a dollar sign survives being # sourced by install exactly as it was typed. record() { printf '%s=%q\n' "$1" "$2" >>"$answers"; } heading() { gum style --bold --foreground 4 "$1"; } ask() { gum input --header "$1" --placeholder "${2:-}"; } yes_no() { gum confirm --default=false "$1"; } # ── Machine ────────────────────────────────────────────────────────────────── heading "This machine" # The role decides most of what follows: a server is never asked about NVIDIA # drivers or Steam, and a desktop is never asked about compose services. It is # also the one answer that outlives the run -- install records it durably, # because `panama update` asks nothing and still has to know which machine it # is updating. See setup/lib/machine-role. # # PANAMA_ROLE_PRESET is how `./install --server` answers this without a prompt, # for the curl-onto-a-fresh-VPS path where the caller already said what the # machine is. An empty or escaped choice falls back to desktop, which is what # every Panama machine was before roles existed. role="${PANAMA_ROLE_PRESET:-}" if [[ -z "$role" ]]; then role="$(gum choose --header "What is this machine?" "desktop" "server")" || role="" fi [[ "$role" == server ]] || role=desktop record PANAMA_ROLE "$role" current_hostname="$(hostname)" printf 'Current hostname: %s\n' "$current_hostname" new_hostname="" if yes_no "Change the hostname?"; then new_hostname="$(ask "Hostname" "$current_hostname")" fi record PANAMA_HOSTNAME "$new_hostname" # ── Identity ───────────────────────────────────────────────────────────────── # # Left blank, each of these keeps whatever git already has. That matters on a # re-run: the prompts start empty every time by design, and an empty answer must # not wipe a name that was already correct. heading "Git identity" printf 'Leave any of these blank to keep the current setting.\n' git_name="$(ask "Git user.name")" git_email="$(ask "Git user.email")" git_editor="$(ask "Git editor" "nvim")" record PANAMA_GIT_NAME "$git_name" record PANAMA_GIT_EMAIL "$git_email" record PANAMA_GIT_EDITOR "$git_editor" # ── Accounts and keys ──────────────────────────────────────────────────────── # # These two are asked only when they would do something. Checking whether a # credential already exists is not the same as remembering a previous answer -- # it is refusing to ask a question whose answer is already on the machine. heading "Accounts" gh_login=no if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then printf 'GitHub CLI is already signed in.\n' elif yes_no "Sign in to GitHub after packages are installed?"; then gh_login=yes fi record PANAMA_GH_LOGIN "$gh_login" ssh_key=no if compgen -G "$HOME/.ssh/id_*.pub" >/dev/null 2>&1; then printf 'An SSH key already exists.\n' elif yes_no "Generate an SSH key?"; then ssh_key=yes fi record PANAMA_SSH_KEY "$ssh_key" # ── Hardware ───────────────────────────────────────────────────────────────── # # Each question names what was detected, so declining is a decision about this # machine rather than an answer to a hypothetical. A machine with no NVIDIA card # is never asked about drivers, and one with nothing to remove is never asked # about removing it. # # A server is asked none of it. The stages these answers feed -- # install-hardware, the debloat removal -- do not run on the server path at # all, and a question whose answer nothing consumes is a control that lies. # The defaults are still recorded so the answers file has the same shape # either way. nvidia=no mok_hash="" installed=() firmware=no debloat=no if [[ "$role" != server ]]; then heading "Hardware" nvidia_card="$(lspci 2>/dev/null | grep -iE 'vga compatible|3d controller' | grep -i nvidia | sed 's/.*: //' | head -1)" if [[ -n "$nvidia_card" ]]; then if yes_no "Found $nvidia_card — install the NVIDIA driver?"; then nvidia=yes # Only asked where it does something. On a machine with Secure Boot off, # akmods' signature is never checked and enrolling a key is ceremony. if mokutil --sb-state 2>/dev/null | grep -qi 'secureboot enabled'; then printf 'Secure Boot is on, so the driver must be signed with a key you enroll.\n' printf 'The next boot will ask for this password on a blue screen.\n' if yes_no "Enroll a machine owner key?"; then # Hashed here and only the hash recorded. The password never # reaches the answers file, the environment, or a command line # -- mokutil takes a hash file precisely so it does not have to. while :; do first="$(gum input --password --header "MOK password")" if [[ -z "$first" ]]; then printf 'No password given; skipping enrollment.\n' break fi second="$(gum input --password --header "MOK password again")" if [[ "$first" == "$second" ]]; then # Fed on stdin, never as an argument: an argument sits # in /proc//cmdline for any local process to read # while mokutil runs. mokutil prints its two prompts on # stdout too, so the hash is the last line. mok_hash="$(printf '%s\n%s\n' "$first" "$first" \ | mokutil --generate-hash | tail -n 1)" break fi printf 'Those did not match.\n' done unset first second fi fi fi else printf 'No NVIDIA card found.\n' fi # The stage that removes them owns the list, so there is one copy of it. mapfile -t removable < <("$(dirname "${BASH_SOURCE[0]}")/install-hardware" --debloat-list) for package in "${removable[@]}"; do rpm -q "$package" >/dev/null 2>&1 && installed+=("$package") done if (( ${#installed[@]} > 0 )); then if yes_no "Remove Fedora's preinstalled extras (${installed[*]})?"; then debloat=yes fi fi if command -v fwupdmgr >/dev/null 2>&1; then if yes_no "Update firmware with fwupdmgr?"; then firmware=yes fi fi fi # role != server record PANAMA_NVIDIA "$nvidia" record PANAMA_MOK_HASH "$mok_hash" record PANAMA_DEBLOAT "$debloat" record PANAMA_FIRMWARE "$firmware" # ── Applications ───────────────────────────────────────────────────────────── # # Everything else in this repository is what every Panama machine gets. This is # the one question about what this machine is for: a work laptop should not # acquire emulators and a desktop should not skip Steam. # # The categories are read from the directory rather than listed here, so adding # one is adding a file. Nothing is preselected -- a default here would install # applications nobody chose, on a machine whose owner answered a question they # thought was about something else. extras="" if [[ "$role" != server ]]; then heading "Applications" extras_dir="$(dirname "${BASH_SOURCE[0]}")/../packages/extras" if [[ -d "$extras_dir" ]]; then mapfile -t categories < <(for file in "$extras_dir"/*; do [[ -f "$file" ]] && basename "$file" done) if (( ${#categories[@]} > 0 )); then printf 'Optional application categories. Space to select, enter to accept.\n' extras="$(gum choose --no-limit --header "Extras" "${categories[@]}" | tr '\n' ' ')" extras="${extras% }" fi fi fi # role != server record PANAMA_EXTRAS "$extras" # ── Personal content ───────────────────────────────────────────────────────── # # user/ holds whoever-owns-this-checkout's personal files: agent instructions, # SSH host aliases, expansion triggers. Linking them is how one person keeps # several machines identical, and it is exactly the wrong thing to do to # somebody who just cloned this repository to try the desktop out. # # So it is asked rather than assumed, the question names what it would link, # and no is the default. Someone who forks Panama replaces user/ with their own # and starts answering yes. user_content=no if [[ -r "$(dirname "${BASH_SOURCE[0]}")/../../user/manifest" ]]; then mapfile -t user_targets < <( grep -vE '^\s*(#|$)' "$(dirname "${BASH_SOURCE[0]}")/../../user/manifest" \ | awk '{ print $3 }' | sort -u ) if (( ${#user_targets[@]} > 0 )); then printf 'This checkout carries personal content for: %s\n' "${user_targets[*]}" printf 'Say no unless this checkout is yours.\n' if yes_no "Link this checkout's personal content into your home?"; then user_content=yes fi fi fi record PANAMA_USER_CONTENT "$user_content" # ── Confirm ────────────────────────────────────────────────────────────────── # # The last chance to catch a typo before twenty minutes of package work that # nobody is watching. shown() { [[ -n "$1" ]] && printf '%s' "$1" || printf 'unchanged'; } heading "Ready" gum style --border rounded --padding "0 1" "$( printf 'Role %s\n' "$role" printf 'Hostname %s\n' "${new_hostname:-"$current_hostname (unchanged)"}" printf 'Git name %s\n' "$(shown "$git_name")" printf 'Git email %s\n' "$(shown "$git_email")" printf 'Git editor %s\n' "$(shown "$git_editor")" printf 'GitHub %s\n' "$([[ "$gh_login" == yes ]] && echo "sign in" || echo "no change")" printf 'SSH key %s\n' "$([[ "$ssh_key" == yes ]] && echo "generate" || echo "no change")" # Hardware and extras were never asked on a server, and a summary line for # a question that was not asked reads as a decision that was not made. if [[ "$role" != server ]]; then printf 'NVIDIA %s\n' "$([[ "$nvidia" == yes ]] && echo "install driver" || echo "no")" printf 'Secure Boot %s\n' "$([[ -n "$mok_hash" ]] && echo "enroll a key" || echo "no change")" printf 'Fedora apps %s\n' "$([[ "$debloat" == yes ]] && echo "remove ${installed[*]}" || echo "keep")" printf 'Firmware %s\n' "$([[ "$firmware" == yes ]] && echo "update" || echo "no")" printf 'Extras %s\n' "${extras:-none}" fi printf 'Personal %s' "$([[ "$user_content" == yes ]] && echo "link user/ into home" || echo "not linked")" )" if ! gum confirm --default=true "Install with these answers?"; then printf 'interview: cancelled; nothing was installed.\n' >&2 exit 1 fi