setup: add answerfile system for fully automated installs
tui-install.sh:
- Reads /answerfile.json if present (ANSWERFILE_MODE)
- All dialog selections (components, DE, apps) sourced from file
- Hostname from answerfile gets MAC address suffix appended to
prevent conflicts when deploying one image to multiple machines
- Interactive hostname inputbox added to the normal TUI flow
- Colorway dialog added as final step; skipped if no colors differ
from defaults and no answerfile colors are set
- Answerfile mode: runs non-interactively, logs warnings on failure
generate-answerfile.sh (new):
- Dry-runs the full installer dialog flow (OS + dotfiles)
- Writes selections to ~/answerfile.json (or a given path)
- No software is installed; passwords are never written to the file
build.sh:
- New --preconf [FILE] flag embeds an answerfile into the ISO at
/answerfile.json; omitting the flag leaves the ISO clean
- Validates JSON with jq if available before embedding
- Reworked arg parsing to handle the new flag alongside OUT_DIR
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
parent
e25dd231cb
commit
b5a3b46c79
|
|
@ -1,10 +1,48 @@
|
|||
#!/usr/bin/env bash
|
||||
# build.sh — build the M-Archy Arch Linux ISO
|
||||
#
|
||||
# Usage:
|
||||
# bash build.sh [--preconf [FILE]] [OUT_DIR]
|
||||
#
|
||||
# --preconf Embed ~/answerfile.json into the ISO at /answerfile.json
|
||||
# --preconf FILE Embed the specified answerfile instead
|
||||
# OUT_DIR Output directory (default: ~/m-archy-out, or $OUT_DIR env)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DOTFILES_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
# ── Argument parsing ───────────────────────────────────────────────────────────
|
||||
PRECONF_FILE=""
|
||||
OUT_ARG=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--preconf)
|
||||
# Optional next arg: a file path (doesn't start with -)
|
||||
if [[ $# -gt 1 && "${2:0:1}" != "-" ]]; then
|
||||
PRECONF_FILE="$2"; shift
|
||||
else
|
||||
PRECONF_FILE="$HOME/answerfile.json"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
--preconf=*)
|
||||
PRECONF_FILE="${1#--preconf=}"
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown flag: $1" >&2; exit 1
|
||||
;;
|
||||
*)
|
||||
OUT_ARG="$1"; shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
WORK_DIR="${WORK_DIR:-$HOME/m-archy-build}"
|
||||
OUT_DIR="${1:-${OUT_DIR:-$HOME/m-archy-out}}"
|
||||
OUT_DIR="${OUT_ARG:-${OUT_DIR:-$HOME/m-archy-out}}"
|
||||
PROFILE="$WORK_DIR/profile"
|
||||
RELENG="/usr/share/archiso/configs/releng"
|
||||
|
||||
|
|
@ -15,6 +53,16 @@ fi
|
|||
|
||||
[[ -d "$RELENG" ]] || { echo "ERROR: $RELENG not found — is archiso installed?"; exit 1; }
|
||||
|
||||
# Validate answerfile early if --preconf was given
|
||||
if [[ -n "$PRECONF_FILE" ]]; then
|
||||
[[ -f "$PRECONF_FILE" ]] \
|
||||
|| { echo "ERROR: answerfile not found: $PRECONF_FILE"; exit 1; }
|
||||
command -v jq &>/dev/null \
|
||||
&& jq empty "$PRECONF_FILE" \
|
||||
|| echo "Warning: jq not available — skipping answerfile JSON validation"
|
||||
echo "Answerfile to embed: $PRECONF_FILE"
|
||||
fi
|
||||
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$WORK_DIR" "$OUT_DIR"
|
||||
|
||||
|
|
@ -43,9 +91,19 @@ chmod 755 \
|
|||
"$PROFILE/airootfs/usr/local/bin/install-arch" \
|
||||
"$PROFILE/airootfs/root/installer/"*.sh
|
||||
|
||||
# ── Embed answerfile (--preconf) ───────────────────────────────────────────────
|
||||
if [[ -n "$PRECONF_FILE" ]]; then
|
||||
echo "Embedding answerfile: $PRECONF_FILE → /answerfile.json"
|
||||
install -m 644 "$PRECONF_FILE" "$PROFILE/airootfs/answerfile.json"
|
||||
fi
|
||||
|
||||
echo "Building ISO (this may take a while)..."
|
||||
sudo mkarchiso -v -w "$WORK_DIR/mkarchiso" -o "$OUT_DIR" "$PROFILE"
|
||||
|
||||
echo
|
||||
echo "Done."
|
||||
ls -lh "$OUT_DIR/"*.iso 2>/dev/null || true
|
||||
|
||||
if [[ -n "$PRECONF_FILE" ]]; then
|
||||
echo "Answerfile embedded — automated install will activate on boot."
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -0,0 +1,377 @@
|
|||
#!/bin/bash
|
||||
# generate-answerfile.sh — dry-run the M-Archy installer dialogs and write
|
||||
# all selections to an answerfile.json. Nothing is installed.
|
||||
#
|
||||
# Usage:
|
||||
# bash generate-answerfile.sh [OUTPUT_PATH]
|
||||
# OUTPUT_PATH defaults to ~/answerfile.json
|
||||
#
|
||||
# The generated file can be placed at /answerfile.json on the installer USB
|
||||
# (use build.sh --preconf to embed it automatically) so that the full
|
||||
# install — base OS + dotfiles — runs without any manual input.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DOTFILES_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
OUTPUT="${1:-$HOME/answerfile.json}"
|
||||
TMP_D="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_D"' EXIT
|
||||
|
||||
BACKTITLE="M-Archy Answerfile Generator"
|
||||
|
||||
# ── Dialog theme ──────────────────────────────────────────────────────────────
|
||||
export DIALOGRC="$TMP_D/dialogrc"
|
||||
cat > "$DIALOGRC" <<'EOF'
|
||||
use_shadow = ON
|
||||
use_colors = ON
|
||||
screen_color = (BLACK,BLACK,ON)
|
||||
shadow_color = (BLACK,BLACK,ON)
|
||||
title_color = (MAGENTA,BLACK,ON)
|
||||
border_color = (MAGENTA,BLACK,ON)
|
||||
button_active_color = (BLACK,MAGENTA,ON)
|
||||
button_inactive_color = (WHITE,BLACK,OFF)
|
||||
button_key_active_color = (BLACK,CYAN,ON)
|
||||
button_key_inactive_color = (CYAN,BLACK,ON)
|
||||
button_label_active_color = (BLACK,MAGENTA,ON)
|
||||
button_label_inactive_color = (WHITE,BLACK,OFF)
|
||||
inputbox_color = (WHITE,BLACK,OFF)
|
||||
inputbox_border_color = (MAGENTA,BLACK,ON)
|
||||
menubox_color = (WHITE,BLACK,OFF)
|
||||
menubox_border_color = (MAGENTA,BLACK,ON)
|
||||
item_color = (WHITE,BLACK,OFF)
|
||||
item_selected_color = (BLACK,MAGENTA,ON)
|
||||
tag_color = (CYAN,BLACK,ON)
|
||||
tag_selected_color = (BLACK,CYAN,ON)
|
||||
tag_key_color = (CYAN,BLACK,ON)
|
||||
tag_key_selected_color = (BLACK,CYAN,ON)
|
||||
check_color = (WHITE,BLACK,OFF)
|
||||
check_selected_color = (BLACK,MAGENTA,ON)
|
||||
uarrow_color = (MAGENTA,BLACK,ON)
|
||||
darrow_color = (MAGENTA,BLACK,ON)
|
||||
EOF
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
require_dialog() {
|
||||
command -v dialog &>/dev/null && return
|
||||
echo "dialog not found — installing..."
|
||||
sudo pacman -S --noconfirm dialog || { echo "Failed to install dialog."; exit 1; }
|
||||
}
|
||||
|
||||
require_jq() {
|
||||
command -v jq &>/dev/null && return
|
||||
echo "jq not found — installing..."
|
||||
sudo pacman -S --noconfirm jq || { echo "Failed to install jq."; exit 1; }
|
||||
}
|
||||
|
||||
die() { clear; printf "\n Error: %s\n\n" "$1" >&2; exit 1; }
|
||||
|
||||
# json_str: emit a properly-quoted JSON string value (handles empty → null)
|
||||
json_str() {
|
||||
local v="$1"
|
||||
if [[ -z "$v" ]]; then printf 'null'; else printf '"%s"' "$v"; fi
|
||||
}
|
||||
|
||||
# json_bool: emit true/false from YES/NO or true/false input
|
||||
json_bool() {
|
||||
local v="${1,,}"
|
||||
[[ "$v" == "yes" || "$v" == "true" ]] && echo "true" || echo "false"
|
||||
}
|
||||
|
||||
# ── Preflight ─────────────────────────────────────────────────────────────────
|
||||
require_dialog
|
||||
require_jq
|
||||
|
||||
# ── Welcome ───────────────────────────────────────────────────────────────────
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title " Answerfile Generator " \
|
||||
--msgbox "\n\
|
||||
This tool walks you through all installer dialogs\n\
|
||||
and saves your choices to an answerfile.\n\
|
||||
─────────────────────────────────────────────────\n\
|
||||
\n\
|
||||
NO software will be installed — this is a dry run.\n\
|
||||
\n\
|
||||
Output: $OUTPUT\n\
|
||||
\n\
|
||||
To use on a USB: build.sh --preconf $OUTPUT\n" 16 62
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# PART 1 — Base OS install options
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# ── Drive ─────────────────────────────────────────────────────────────────────
|
||||
AVAIL_DRIVES=$(lsblk -dn -o NAME,SIZE,MODEL 2>/dev/null | awk '{printf "%s \"%s %s\" off ", $1, $2, $3}' || true)
|
||||
|
||||
AF_DRIVE=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Target Drive " \
|
||||
--inputbox "\n Enter the install drive (e.g. /dev/sda, /dev/nvme0n1).\n\n Available drives:\n$(lsblk -dn -o NAME,SIZE,MODEL 2>/dev/null | sed 's/^/ /')\n" \
|
||||
16 64 "/dev/sda" \
|
||||
3>&1 1>&2 2>&3) || AF_DRIVE=""
|
||||
|
||||
# ── Kernel ────────────────────────────────────────────────────────────────────
|
||||
AF_KERNEL=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Kernel " \
|
||||
--menu "Select kernel package:" 12 54 3 \
|
||||
"linux" "Stable kernel" \
|
||||
"linux-lts" "Long-term support kernel" \
|
||||
"linux-zen" "Zen performance kernel" \
|
||||
3>&1 1>&2 2>&3) || AF_KERNEL="linux"
|
||||
|
||||
# ── Hostname ──────────────────────────────────────────────────────────────────
|
||||
AF_HOSTNAME=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Hostname " \
|
||||
--inputbox "\n Hostname for the new system.\n\n Leave blank to keep default.\n\n Note: a MAC address suffix will be appended\n automatically when the answerfile is applied,\n preventing hostname conflicts across machines.\n" \
|
||||
14 62 "" \
|
||||
3>&1 1>&2 2>&3) || AF_HOSTNAME=""
|
||||
|
||||
# ── Username ──────────────────────────────────────────────────────────────────
|
||||
AF_USERNAME=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Username " \
|
||||
--inputbox "\n Name for the primary user account.\n" \
|
||||
9 54 "" \
|
||||
3>&1 1>&2 2>&3) || AF_USERNAME=""
|
||||
|
||||
# NOTE: passwords are intentionally NOT stored in the answerfile.
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title " Password " \
|
||||
--msgbox "\n Passwords are NOT stored in the answerfile.\n\n You will be prompted for the user password\n at install time even in automated mode.\n" \
|
||||
10 56
|
||||
|
||||
# ── Disk encryption ───────────────────────────────────────────────────────────
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title " Disk Encryption " \
|
||||
--yesno "\n Enable LUKS2 disk encryption on the root partition?\n\n If yes, a backup LUKS key will be auto-generated\n and placed at /_LUKS_BACKUP_KEY in the new system.\n" \
|
||||
11 62 && AF_ENCRYPT="true" || AF_ENCRYPT="false"
|
||||
|
||||
AF_FIDO2_ROOT="false"
|
||||
AF_FIDO2_USER="false"
|
||||
if [[ "$AF_ENCRYPT" == "true" ]]; then
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title " FIDO2 Root Unlock " \
|
||||
--yesno "\n Enable FIDO2 hardware key for LUKS root unlock?\n" \
|
||||
8 56 && AF_FIDO2_ROOT="true" || AF_FIDO2_ROOT="false"
|
||||
fi
|
||||
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title " FIDO2 User Login " \
|
||||
--yesno "\n Enable FIDO2 hardware key for user login (PAM)?\n" \
|
||||
8 56 && AF_FIDO2_USER="true" || AF_FIDO2_USER="false"
|
||||
|
||||
# ── Run TUI installer after base install ──────────────────────────────────────
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title " Dotfiles Setup " \
|
||||
--yesno "\n Automatically run the dotfiles TUI installer\n inside the chroot after base install completes?\n" \
|
||||
9 58 && AF_RUN_TUI="true" || AF_RUN_TUI="false"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# PART 2 — Dotfiles / TUI installer options
|
||||
# (only shown if run_tui is true)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
AF_COMPONENTS=""
|
||||
AF_DE="none"
|
||||
AF_APPS=""
|
||||
AF_COLOR_TEXT=""
|
||||
AF_COLOR_BG=""
|
||||
AF_COLOR_HIGHLIGHT=""
|
||||
AF_COLOR_DARK=""
|
||||
AF_COLOR_RED=""
|
||||
|
||||
if [[ "$AF_RUN_TUI" == "true" ]]; then
|
||||
|
||||
# ── Components ────────────────────────────────────────────────────────────
|
||||
AF_COMPONENTS=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Select Components " \
|
||||
--checklist "Space toggles · Enter confirms · Esc quits" 15 68 4 \
|
||||
"pkg" "Package managers yay · nvm · rust" on \
|
||||
"core" "Core packages 100+ base system packages" on \
|
||||
"svc" "Core services NetworkManager · cronie · fail2ban" on \
|
||||
"shell" "Shell setup zsh · nvim · yazi · micro · starship" on \
|
||||
3>&1 1>&2 2>&3) || AF_COMPONENTS=""
|
||||
|
||||
# ── DE ────────────────────────────────────────────────────────────────────
|
||||
AF_DE=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Desktop Environment " \
|
||||
--menu "Select a desktop environment · Esc / none to skip:" 20 70 8 \
|
||||
"hyprland" "Hyprland — Wayland WM, full setup (primary)" \
|
||||
"sway" "Sway — Wayland tiling WM" \
|
||||
"kde-plasma" "KDE Plasma — feature-rich Wayland/X11 DE" \
|
||||
"gnome" "GNOME — modern Wayland DE" \
|
||||
"cosmic" "COSMIC — Rust-built Wayland DE (System76)" \
|
||||
"xfce" "XFCE — lightweight X11 DE" \
|
||||
"lxqt" "LXQt — lightweight Qt X11 DE" \
|
||||
"none" "Skip DE installation" \
|
||||
3>&1 1>&2 2>&3) || AF_DE="none"
|
||||
|
||||
# ── Apps ──────────────────────────────────────────────────────────────────
|
||||
AF_APPS=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Applications " \
|
||||
--checklist "Optional applications — installed after base components:" 40 76 32 \
|
||||
"ollama" "Ollama local LLM runner + API server" off \
|
||||
"llama-cpp" "llama.cpp standalone inference CLI + server" off \
|
||||
"open-webui" "Open WebUI browser UI for Ollama / LLM backends" off \
|
||||
"claude" "Claude Code Anthropic CLI via npm" off \
|
||||
"networking-cli" "Networking CLI nmap · nethogs · mitmproxy · httpie" off \
|
||||
"disk-recovery" "Disk Recovery ddrescue · f3" off \
|
||||
"himalaya" "Himalaya terminal email client (AUR)" off \
|
||||
"gnuplot" "Gnuplot scientific plotting" off \
|
||||
"povray" "POV-Ray ray-tracing renderer" off \
|
||||
"blender" "Blender 3D creation suite" off \
|
||||
"toot" "toot Mastodon CLI client (AUR)" off \
|
||||
"db-clients" "DB Clients pgcli · mycli" off \
|
||||
"mysql" "MySQL / MariaDB mariadb server + setup" off \
|
||||
"productivity" "Productivity taskwarrior · watson · jrnl" off \
|
||||
"yt-dlp" "yt-dlp YouTube / media downloader" off \
|
||||
"sox" "SoX audio processing toolkit" off \
|
||||
"imagemagick" "ImageMagick image manipulation" off \
|
||||
"ffmpeg" "FFmpeg extras thumbnailer · GStreamer codecs" off \
|
||||
"localtunnel" "LocalTunnel expose localhost via tunnel" off \
|
||||
"butter" "butter btrfs snapshot backup (AUR)" off \
|
||||
"tlp" "TLP laptop power management" off \
|
||||
"steam" "Steam gaming platform" off \
|
||||
"vesktop" "Vesktop Discord + Vencord theme" off \
|
||||
"spotify" "Spotify launcher + Spicetify theming" off \
|
||||
"prism" "PrismLauncher Minecraft launcher (Flatpak)" off \
|
||||
"vintagestory" "Vintage Story survival game (AUR)" off \
|
||||
"localsend" "LocalSend LAN file transfer (AUR)" off \
|
||||
"croc" "croc cross-platform file transfer" off \
|
||||
"onlyoffice" "OnlyOffice office suite (AUR)" off \
|
||||
"wireshark" "Wireshark network packet analyser (GUI)" off \
|
||||
"k8s" "Kubernetes tools kubectl · podman-desktop" off \
|
||||
"docker" "Docker docker · docker-compose" off \
|
||||
"podman" "Podman rootless containers · buildah" off \
|
||||
"cockpit" "Cockpit web UI · machines · podman" off \
|
||||
"ssh-server" "SSH server openssh · key-auth · enabled" off \
|
||||
"freeipa-client" "FreeIPA Client sssd + ipa-client-install + enrollment" off \
|
||||
"freeipa-server" "FreeIPA Server interactive server setup + client gen" off \
|
||||
"freeipa-image" "FreeIPA Image OCI/LXC/Proxmox LXC builder + Keycloak" off \
|
||||
"python" "Python tools pyright · pipx · pynvim" off \
|
||||
"zfs" "ZFS zfs-dkms kernel module" off \
|
||||
"wprs" "WPRS wprs-git (AUR)" off \
|
||||
"chromium" "Chromium open-source browser (official)" off \
|
||||
"firefox-browser" "Firefox Mozilla browser (official)" off \
|
||||
"zen-browser" "Zen Browser Firefox-based privacy browser (AUR)" off \
|
||||
"nyxt" "Nyxt keyboard-driven browser (AUR)" off \
|
||||
"librewolf" "LibreWolf hardened Firefox fork (AUR)" off \
|
||||
"min-browser" "Min minimal Electron browser (AUR)" off \
|
||||
"vscodium" "VSCodium telemetry-free VS Code (AUR)" off \
|
||||
"zed-ide" "Zed high-performance Rust IDE (official)" off \
|
||||
"geany" "Geany lightweight IDE + plugins (official)" off \
|
||||
"codeblocks" "Code::Blocks C/C++ IDE (official)" off \
|
||||
"kate" "Kate KDE advanced text editor (official)" off \
|
||||
3>&1 1>&2 2>&3) || AF_APPS=""
|
||||
|
||||
# ── Colorway ──────────────────────────────────────────────────────────────
|
||||
# Read defaults from repo colors.conf
|
||||
declare -A _cdef
|
||||
if [[ -f "$DOTFILES_DIR/colors.conf" ]]; then
|
||||
while IFS='=' read -r k v; do
|
||||
k="${k%%[[:space:]]*}"
|
||||
[[ "$k" =~ ^[[:space:]]*# || -z "$k" ]] && continue
|
||||
v="${v%%#*}"; v="${v//[[:space:]]/}"; v="${v^^}"
|
||||
_cdef[$k]="$v"
|
||||
done < "$DOTFILES_DIR/colors.conf"
|
||||
fi
|
||||
DEF_TEXT="${_cdef[COLOR_TEXT]:-D6ABAB}"
|
||||
DEF_BG="${_cdef[COLOR_BG]:-1A1A1A}"
|
||||
DEF_HIGHLIGHT="${_cdef[COLOR_HIGHLIGHT]:-E40046}"
|
||||
DEF_DARK="${_cdef[COLOR_DARK]:-5018DD}"
|
||||
DEF_RED="${_cdef[COLOR_RED]:-F50505}"
|
||||
|
||||
COLORWAY_RAW=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Colorway (optional) " \
|
||||
--form "\n Customize theme colors — bare 6-digit hex, no #.\n Leave unchanged to omit colors from answerfile.\n" \
|
||||
16 62 5 \
|
||||
"COLOR_TEXT " 1 1 "$DEF_TEXT" 1 18 10 7 \
|
||||
"COLOR_BG " 2 1 "$DEF_BG" 2 18 10 7 \
|
||||
"COLOR_HIGHLIGHT " 3 1 "$DEF_HIGHLIGHT" 3 18 10 7 \
|
||||
"COLOR_DARK " 4 1 "$DEF_DARK" 4 18 10 7 \
|
||||
"COLOR_RED " 5 1 "$DEF_RED" 5 18 10 7 \
|
||||
3>&1 1>&2 2>&3) || COLORWAY_RAW=""
|
||||
|
||||
if [[ -n "$COLORWAY_RAW" ]]; then
|
||||
mapfile -t _cv <<< "$COLORWAY_RAW"
|
||||
N_TEXT="${_cv[0]:-$DEF_TEXT}"
|
||||
N_BG="${_cv[1]:-$DEF_BG}"
|
||||
N_HIGHLIGHT="${_cv[2]:-$DEF_HIGHLIGHT}"
|
||||
N_DARK="${_cv[3]:-$DEF_DARK}"
|
||||
N_RED="${_cv[4]:-$DEF_RED}"
|
||||
# Only save colors if any differ from defaults
|
||||
if [[ "${N_TEXT^^}" != "$DEF_TEXT" || \
|
||||
"${N_BG^^}" != "$DEF_BG" || \
|
||||
"${N_HIGHLIGHT^^}" != "$DEF_HIGHLIGHT" || \
|
||||
"${N_DARK^^}" != "$DEF_DARK" || \
|
||||
"${N_RED^^}" != "$DEF_RED" ]]; then
|
||||
AF_COLOR_TEXT="${N_TEXT^^}"
|
||||
AF_COLOR_BG="${N_BG^^}"
|
||||
AF_COLOR_HIGHLIGHT="${N_HIGHLIGHT^^}"
|
||||
AF_COLOR_DARK="${N_DARK^^}"
|
||||
AF_COLOR_RED="${N_RED^^}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Confirmation ──────────────────────────────────────────────────────────────
|
||||
SUMMARY=""
|
||||
[[ -n "$AF_DRIVE" ]] && SUMMARY+=" Drive: $AF_DRIVE\n"
|
||||
[[ -n "$AF_KERNEL" ]] && SUMMARY+=" Kernel: $AF_KERNEL\n"
|
||||
[[ -n "$AF_HOSTNAME" ]] && SUMMARY+=" Hostname: $AF_HOSTNAME (+ MAC suffix at deploy)\n"
|
||||
[[ -n "$AF_USERNAME" ]] && SUMMARY+=" Username: $AF_USERNAME\n"
|
||||
SUMMARY+=" Encrypt: $AF_ENCRYPT\n"
|
||||
SUMMARY+=" FIDO2 root: $AF_FIDO2_ROOT / FIDO2 user: $AF_FIDO2_USER\n"
|
||||
SUMMARY+=" Run TUI: $AF_RUN_TUI\n"
|
||||
[[ -n "$AF_DE" && "$AF_DE" != "none" ]] && SUMMARY+=" DE: $AF_DE\n"
|
||||
[[ -n "$AF_COLOR_TEXT" ]] && SUMMARY+=" Colors: custom\n"
|
||||
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title " Confirm " \
|
||||
--yesno "\n Save answerfile with these settings:\n\n${SUMMARY}\n Output: $OUTPUT\n\n Proceed?" \
|
||||
22 66 || { clear; echo "Aborted."; exit 0; }
|
||||
|
||||
# ── Build JSON arrays from space-separated dialog output ──────────────────────
|
||||
_words_to_json_array() {
|
||||
local input="$1"
|
||||
local first=1
|
||||
printf '['
|
||||
for w in $input; do
|
||||
[[ $first -eq 0 ]] && printf ','
|
||||
printf '"%s"' "$w"
|
||||
first=0
|
||||
done
|
||||
printf ']'
|
||||
}
|
||||
|
||||
# ── Write answerfile ──────────────────────────────────────────────────────────
|
||||
mkdir -p "$(dirname "$OUTPUT")"
|
||||
|
||||
{
|
||||
printf '{\n'
|
||||
printf ' "_generated": "%s",\n' "$(date -Iseconds)"
|
||||
printf ' "drive": %s,\n' "$(json_str "$AF_DRIVE")"
|
||||
printf ' "kernel": %s,\n' "$(json_str "$AF_KERNEL")"
|
||||
printf ' "hostname": %s,\n' "$(json_str "$AF_HOSTNAME")"
|
||||
printf ' "username": %s,\n' "$(json_str "$AF_USERNAME")"
|
||||
printf ' "encrypt": %s,\n' "$AF_ENCRYPT"
|
||||
printf ' "fido2_root": %s,\n' "$AF_FIDO2_ROOT"
|
||||
printf ' "fido2_user": %s,\n' "$AF_FIDO2_USER"
|
||||
printf ' "run_tui": %s,\n' "$AF_RUN_TUI"
|
||||
printf ' "components": %s,\n' "$(_words_to_json_array "$AF_COMPONENTS")"
|
||||
printf ' "desktop_environment": %s,\n' "$(json_str "$AF_DE")"
|
||||
printf ' "apps": %s' "$(_words_to_json_array "$AF_APPS")"
|
||||
|
||||
if [[ -n "$AF_COLOR_TEXT" ]]; then
|
||||
printf ',\n "colors": {\n'
|
||||
printf ' "COLOR_TEXT": "%s",\n' "$AF_COLOR_TEXT"
|
||||
printf ' "COLOR_BG": "%s",\n' "$AF_COLOR_BG"
|
||||
printf ' "COLOR_HIGHLIGHT": "%s",\n' "$AF_COLOR_HIGHLIGHT"
|
||||
printf ' "COLOR_DARK": "%s",\n' "$AF_COLOR_DARK"
|
||||
printf ' "COLOR_RED": "%s"\n' "$AF_COLOR_RED"
|
||||
printf ' }'
|
||||
fi
|
||||
|
||||
printf '\n}\n'
|
||||
} > "$OUTPUT"
|
||||
|
||||
clear
|
||||
printf "\n Answerfile saved to: %s\n\n" "$OUTPUT"
|
||||
printf " To embed in ISO: bash setup/archiso/build.sh --preconf %s\n\n" "$OUTPUT"
|
||||
|
|
@ -12,6 +12,10 @@ LOG="$HOME/dotfiles-install.log"
|
|||
TMP_D="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_D"' EXIT
|
||||
|
||||
ANSWERFILE="${ANSWERFILE:-/answerfile.json}"
|
||||
ANSWERFILE_MODE=false
|
||||
[[ -f "$ANSWERFILE" ]] && ANSWERFILE_MODE=true
|
||||
|
||||
BACKTITLE="the_miro's Arch Dotfiles"
|
||||
|
||||
# ── Cyberqueer dialog theme ───────────────────────────────────────────────────
|
||||
|
|
@ -56,6 +60,12 @@ require_dialog() {
|
|||
sudo pacman -S --noconfirm dialog || { echo "Failed to install dialog."; exit 1; }
|
||||
}
|
||||
|
||||
require_jq() {
|
||||
command -v jq &>/dev/null && return
|
||||
echo "jq not found — installing..."
|
||||
sudo pacman -S --noconfirm jq || { echo "Failed to install jq."; exit 1; }
|
||||
}
|
||||
|
||||
die() {
|
||||
clear
|
||||
printf "\n Error: %s\n\n" "$1" >&2
|
||||
|
|
@ -79,11 +89,15 @@ run_module() {
|
|||
bash "$script" 2>&1 | tee -a "$LOG" || rc=${PIPESTATUS[0]}
|
||||
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
if [[ $ANSWERFILE_MODE == true ]]; then
|
||||
printf "\n Warning: %s exited with code %d — continuing.\n" "$label" "$rc" | tee -a "$LOG"
|
||||
else
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title " Module Failed " \
|
||||
--yesno "$label exited with code $rc.\n\nContinue anyway?" 8 54 \
|
||||
|| { clear; exit 1; }
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
count_steps() {
|
||||
|
|
@ -148,13 +162,53 @@ count_steps() {
|
|||
[[ "$a" == *"kate"* ]] && TOTAL=$(( TOTAL + 1 ))
|
||||
}
|
||||
|
||||
# ── Answerfile ────────────────────────────────────────────────────────────────
|
||||
AF_HOSTNAME=""
|
||||
AF_COMPONENTS=""
|
||||
AF_DE="none"
|
||||
AF_APPS=""
|
||||
AF_COLOR_TEXT=""
|
||||
AF_COLOR_BG=""
|
||||
AF_COLOR_HIGHLIGHT=""
|
||||
AF_COLOR_DARK=""
|
||||
AF_COLOR_RED=""
|
||||
|
||||
load_answerfile() {
|
||||
require_jq
|
||||
AF_HOSTNAME=$(jq -r '.hostname // ""' "$ANSWERFILE")
|
||||
AF_COMPONENTS=$(jq -r '(.components // []) | join(" ")' "$ANSWERFILE")
|
||||
AF_DE=$(jq -r '.desktop_environment // "none"' "$ANSWERFILE")
|
||||
AF_APPS=$(jq -r '(.apps // []) | join(" ")' "$ANSWERFILE")
|
||||
AF_COLOR_TEXT=$(jq -r '.colors.COLOR_TEXT // ""' "$ANSWERFILE")
|
||||
AF_COLOR_BG=$(jq -r '.colors.COLOR_BG // ""' "$ANSWERFILE")
|
||||
AF_COLOR_HIGHLIGHT=$(jq -r '.colors.COLOR_HIGHLIGHT // ""' "$ANSWERFILE")
|
||||
AF_COLOR_DARK=$(jq -r '.colors.COLOR_DARK // ""' "$ANSWERFILE")
|
||||
AF_COLOR_RED=$(jq -r '.colors.COLOR_RED // ""' "$ANSWERFILE")
|
||||
}
|
||||
|
||||
# ── MAC address helper ────────────────────────────────────────────────────────
|
||||
get_mac_suffix() {
|
||||
local mac
|
||||
mac=$(ip link show 2>/dev/null \
|
||||
| awk '/^[0-9]+: [^l][^o]/{iface=1} iface && /link\/ether/{print $2; iface=0; exit}')
|
||||
printf '%s' "${mac//:/}"
|
||||
}
|
||||
|
||||
# ── Preflight ─────────────────────────────────────────────────────────────────
|
||||
[[ $EUID -eq 0 ]] && die "Run as your normal user (not root)."
|
||||
command -v pacman &>/dev/null || die "pacman not found — Arch Linux required."
|
||||
|
||||
require_dialog
|
||||
|
||||
if $ANSWERFILE_MODE; then
|
||||
load_answerfile
|
||||
printf "Answerfile mode: %s\n" "$ANSWERFILE" | tee -a "$LOG"
|
||||
fi
|
||||
|
||||
if ! ping -c1 -W3 archlinux.org &>/dev/null; then
|
||||
if $ANSWERFILE_MODE; then
|
||||
printf "Warning: no internet connection detected.\n" | tee -a "$LOG"
|
||||
else
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title " No Network Detected " \
|
||||
--msgbox "\n No internet connection found.\n\n nmtui will open so you can configure networking.\n Close nmtui when done to continue the installer.\n" 11 58
|
||||
|
|
@ -166,11 +220,13 @@ if ! ping -c1 -W3 archlinux.org &>/dev/null; then
|
|||
|| { clear; echo "Aborted — no network."; exit 1; }
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
> "$LOG"
|
||||
printf "Dotfiles install: %s\nDotfiles dir: %s\n" "$(date)" "$DOTFILES_DIR" >> "$LOG"
|
||||
|
||||
# ── Welcome ───────────────────────────────────────────────────────────────────
|
||||
if ! $ANSWERFILE_MODE; then
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title " Welcome " \
|
||||
--msgbox "\n\
|
||||
|
|
@ -182,8 +238,34 @@ dialog --backtitle "$BACKTITLE" \
|
|||
\n\
|
||||
Source: $DOTFILES_DIR\n\
|
||||
Log: $LOG\n" 14 62
|
||||
fi
|
||||
|
||||
# ── Hostname ──────────────────────────────────────────────────────────────────
|
||||
HOSTNAME_SET=""
|
||||
if $ANSWERFILE_MODE; then
|
||||
if [[ -n "$AF_HOSTNAME" ]]; then
|
||||
MAC=$(get_mac_suffix)
|
||||
HOSTNAME_SET="${AF_HOSTNAME}-${MAC}"
|
||||
printf "Hostname (from answerfile + MAC): %s\n" "$HOSTNAME_SET" | tee -a "$LOG"
|
||||
fi
|
||||
else
|
||||
HOSTNAME_INPUT=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Hostname " \
|
||||
--inputbox "\n Hostname for this machine (leave blank to keep default).\n" 9 54 "" \
|
||||
3>&1 1>&2 2>&3) || HOSTNAME_INPUT=""
|
||||
HOSTNAME_SET="$HOSTNAME_INPUT"
|
||||
fi
|
||||
|
||||
if [[ -n "$HOSTNAME_SET" ]]; then
|
||||
sudo hostnamectl set-hostname "$HOSTNAME_SET" 2>/dev/null \
|
||||
|| echo "$HOSTNAME_SET" | sudo tee /etc/hostname > /dev/null
|
||||
printf "Hostname set: %s\n" "$HOSTNAME_SET" >> "$LOG"
|
||||
fi
|
||||
|
||||
# ── Component selection ───────────────────────────────────────────────────────
|
||||
if $ANSWERFILE_MODE; then
|
||||
COMPONENTS="$AF_COMPONENTS"
|
||||
else
|
||||
COMPONENTS=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Select Components " \
|
||||
--checklist "Space toggles · Enter confirms · Esc quits" 15 68 4 \
|
||||
|
|
@ -192,8 +274,12 @@ COMPONENTS=$(dialog --backtitle "$BACKTITLE" \
|
|||
"svc" "Core services NetworkManager · cronie · fail2ban" on \
|
||||
"shell" "Shell setup zsh · nvim · yazi · micro · starship" on \
|
||||
3>&1 1>&2 2>&3) || { clear; echo "Aborted."; exit 0; }
|
||||
fi
|
||||
|
||||
# ── DE selection ──────────────────────────────────────────────────────────────
|
||||
if $ANSWERFILE_MODE; then
|
||||
DE="$AF_DE"
|
||||
else
|
||||
DE=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Desktop Environment " \
|
||||
--menu "Select a desktop environment · Esc / none to skip:" 20 70 8 \
|
||||
|
|
@ -206,8 +292,12 @@ DE=$(dialog --backtitle "$BACKTITLE" \
|
|||
"lxqt" "LXQt — lightweight Qt X11 DE" \
|
||||
"none" "Skip DE installation" \
|
||||
3>&1 1>&2 2>&3) || DE="none"
|
||||
fi
|
||||
|
||||
# ── Apps selection ────────────────────────────────────────────────────────────
|
||||
if $ANSWERFILE_MODE; then
|
||||
SELECTED_APPS="$AF_APPS"
|
||||
else
|
||||
SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Applications " \
|
||||
--checklist "Optional applications — installed after base components:" 40 76 32 \
|
||||
|
|
@ -266,9 +356,12 @@ SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \
|
|||
"codeblocks" "Code::Blocks C/C++ IDE (official)" off \
|
||||
"kate" "Kate KDE advanced text editor (official)" off \
|
||||
3>&1 1>&2 2>&3) || SELECTED_APPS=""
|
||||
fi
|
||||
|
||||
# ── Confirmation ──────────────────────────────────────────────────────────────
|
||||
# ── Confirmation (interactive mode only) ──────────────────────────────────────
|
||||
if ! $ANSWERFILE_MODE; then
|
||||
SUMMARY=""
|
||||
[[ -n "$HOSTNAME_SET" ]] && SUMMARY+=" ✦ Hostname: $HOSTNAME_SET\n"
|
||||
[[ "$COMPONENTS" == *"pkg"* ]] && SUMMARY+=" ✦ Package managers (yay, nvm, rust)\n"
|
||||
[[ "$COMPONENTS" == *"core"* ]] && SUMMARY+=" ✦ Core packages\n"
|
||||
[[ "$COMPONENTS" == *"svc"* ]] && SUMMARY+=" ✦ Core services\n"
|
||||
|
|
@ -335,6 +428,7 @@ dialog --backtitle "$BACKTITLE" \
|
|||
--title " Confirm Installation " \
|
||||
--yesno "\n Components to install:\n\n${SUMMARY}\n Log: $LOG\n\n Proceed?" \
|
||||
24 62 || { clear; echo "Aborted."; exit 0; }
|
||||
fi
|
||||
|
||||
count_steps "$COMPONENTS" "$DE" "$SELECTED_APPS"
|
||||
|
||||
|
|
@ -410,10 +504,85 @@ fi
|
|||
[[ "$SELECTED_APPS" == *"codeblocks"* ]] && run_module "Code::Blocks" "$APPS/codeblocks.sh"
|
||||
[[ "$SELECTED_APPS" == *"kate"* ]] && run_module "Kate" "$APPS/kate.sh"
|
||||
|
||||
# ── Colorway (final step) ─────────────────────────────────────────────────────
|
||||
# Read defaults from repo colors.conf for pre-population
|
||||
declare -A _cdef
|
||||
if [[ -f "$DOTFILES_DIR/colors.conf" ]]; then
|
||||
while IFS='=' read -r k v; do
|
||||
k="${k%%[[:space:]]*}"
|
||||
[[ "$k" =~ ^[[:space:]]*# || -z "$k" ]] && continue
|
||||
v="${v%%#*}"; v="${v//[[:space:]]/}"; v="${v^^}"
|
||||
_cdef[$k]="$v"
|
||||
done < "$DOTFILES_DIR/colors.conf"
|
||||
fi
|
||||
DEF_TEXT="${_cdef[COLOR_TEXT]:-D6ABAB}"
|
||||
DEF_BG="${_cdef[COLOR_BG]:-1A1A1A}"
|
||||
DEF_HIGHLIGHT="${_cdef[COLOR_HIGHLIGHT]:-E40046}"
|
||||
DEF_DARK="${_cdef[COLOR_DARK]:-5018DD}"
|
||||
DEF_RED="${_cdef[COLOR_RED]:-F50505}"
|
||||
|
||||
_write_colors_conf() {
|
||||
local out="$1" t="$2" b="$3" h="$4" d="$5" r="$6"
|
||||
printf 'COLOR_TEXT=%s\nCOLOR_BG=%s\nCOLOR_HIGHLIGHT=%s\nCOLOR_DARK=%s\nCOLOR_RED=%s\n' \
|
||||
"${t^^}" "${b^^}" "${h^^}" "${d^^}" "${r^^}" > "$out"
|
||||
}
|
||||
|
||||
if $ANSWERFILE_MODE; then
|
||||
# Apply colors from answerfile if any are set
|
||||
if [[ -n "$AF_COLOR_TEXT$AF_COLOR_BG$AF_COLOR_HIGHLIGHT$AF_COLOR_DARK$AF_COLOR_RED" ]]; then
|
||||
TMP_COLORS="$TMP_D/colors.conf"
|
||||
_write_colors_conf "$TMP_COLORS" \
|
||||
"${AF_COLOR_TEXT:-$DEF_TEXT}" \
|
||||
"${AF_COLOR_BG:-$DEF_BG}" \
|
||||
"${AF_COLOR_HIGHLIGHT:-$DEF_HIGHLIGHT}" \
|
||||
"${AF_COLOR_DARK:-$DEF_DARK}" \
|
||||
"${AF_COLOR_RED:-$DEF_RED}"
|
||||
printf "Applying colorway from answerfile...\n" | tee -a "$LOG"
|
||||
bash "$DOTFILES_DIR/apply-theme.sh" "$TMP_COLORS" 2>&1 | tee -a "$LOG" || true
|
||||
fi
|
||||
else
|
||||
# Interactive: show color form dialog
|
||||
COLORWAY_RAW=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Colorway (optional) " \
|
||||
--form "\n Customize theme colors — bare 6-digit hex, no #.\n Leave unchanged to skip colorway setup.\n" \
|
||||
16 62 5 \
|
||||
"COLOR_TEXT " 1 1 "$DEF_TEXT" 1 18 10 7 \
|
||||
"COLOR_BG " 2 1 "$DEF_BG" 2 18 10 7 \
|
||||
"COLOR_HIGHLIGHT " 3 1 "$DEF_HIGHLIGHT" 3 18 10 7 \
|
||||
"COLOR_DARK " 4 1 "$DEF_DARK" 4 18 10 7 \
|
||||
"COLOR_RED " 5 1 "$DEF_RED" 5 18 10 7 \
|
||||
3>&1 1>&2 2>&3) || COLORWAY_RAW=""
|
||||
|
||||
if [[ -n "$COLORWAY_RAW" ]]; then
|
||||
mapfile -t _cv <<< "$COLORWAY_RAW"
|
||||
N_TEXT="${_cv[0]:-$DEF_TEXT}"
|
||||
N_BG="${_cv[1]:-$DEF_BG}"
|
||||
N_HIGHLIGHT="${_cv[2]:-$DEF_HIGHLIGHT}"
|
||||
N_DARK="${_cv[3]:-$DEF_DARK}"
|
||||
N_RED="${_cv[4]:-$DEF_RED}"
|
||||
|
||||
if [[ "${N_TEXT^^}" != "$DEF_TEXT" || \
|
||||
"${N_BG^^}" != "$DEF_BG" || \
|
||||
"${N_HIGHLIGHT^^}" != "$DEF_HIGHLIGHT" || \
|
||||
"${N_DARK^^}" != "$DEF_DARK" || \
|
||||
"${N_RED^^}" != "$DEF_RED" ]]; then
|
||||
TMP_COLORS="$TMP_D/colors.conf"
|
||||
_write_colors_conf "$TMP_COLORS" "$N_TEXT" "$N_BG" "$N_HIGHLIGHT" "$N_DARK" "$N_RED"
|
||||
clear
|
||||
printf "\n\033[1;35m Applying colorway...\033[0m\n\n"
|
||||
bash "$DOTFILES_DIR/apply-theme.sh" "$TMP_COLORS" 2>&1 | tee -a "$LOG" || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||
if $ANSWERFILE_MODE; then
|
||||
printf "\nDone. Log: %s\n" "$LOG"
|
||||
else
|
||||
dialog --backtitle "$BACKTITLE" \
|
||||
--title " Done " \
|
||||
--msgbox "\n All selected components installed.\n\n Log: $LOG\n\n A reboot may be required for all changes to take effect.\n" 12 58
|
||||
|
||||
clear
|
||||
printf "\n Done. Log: %s\n\n" "$LOG"
|
||||
fi
|
||||
|
|
|
|||
Loading…
Reference in New Issue