summaryrefslogtreecommitdiff
path: root/archsetup
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-01-24 13:21:01 -0600
committerCraig Jennings <c@cjennings.net>2026-01-24 13:21:01 -0600
commit6011f90328d88a2c449442d6a31fef3614926ec9 (patch)
tree0d278c1a7ca11d232a7293bae5b2b95a0dad1f01 /archsetup
parent27595e0dc40c2e8daa78d2f057f6f64dc229adc5 (diff)
fix(archsetup): bug fixes, locale support, and code improvements
Bug fixes: - Initialize errors_encountered at script start (not in intro) - Capture correct exit code in retry_install loop - Add missing error_fatal parameters - Fix unclosed quote in error message - Quote variables in pacman_install/aur_install commands - Standardize done statements (remove trailing semicolons) New features: - Locale selection prompt with 8 common options + custom entry - Auto-derive wireless region from locale - Extract zfs-replicate to separate script file - Make archsetup repo URL configurable - Add MulticastDNS=no to avoid avahi conflict Code improvements: - Single STEPS array for show_status and main execution loop - Document security note for config file sourcing - Add explanatory comment for UFW VM behavior - Silence update-desktop-database warnings Config updates: - Add LOCALE and ARCHSETUP_REPO to example config Also adds Wayland/Hyprland desktop alternative to V2MOM roadmap.
Diffstat (limited to 'archsetup')
-rwxr-xr-xarchsetup213
1 files changed, 98 insertions, 115 deletions
diff --git a/archsetup b/archsetup
index d29783c..edd379f 100755
--- a/archsetup
+++ b/archsetup
@@ -98,6 +98,9 @@ load_config() {
exit 1
fi
echo "Loading config from: $config_path"
+ # SECURITY NOTE: source executes config as bash. Only use trusted config files.
+ # This is acceptable because: (1) user explicitly provides path, (2) script runs
+ # as root anyway, (3) if attacker can write config, they likely have root access.
# shellcheck disable=SC1090
source "$config_path"
@@ -114,6 +117,8 @@ load_config() {
[[ -n "$ST_REPO" ]] && st_repo="$ST_REPO"
[[ -n "$SLOCK_REPO" ]] && slock_repo="$SLOCK_REPO"
[[ -n "$DOTEMACS_REPO" ]] && dotemacs_repo="$DOTEMACS_REPO"
+ [[ -n "$ARCHSETUP_REPO" ]] && archsetup_repo="$ARCHSETUP_REPO"
+ [[ -n "$LOCALE" ]] && locale="$LOCALE"
}
# Load config if specified
@@ -124,6 +129,7 @@ load_config() {
username="${username:-cjennings}"
password="${password:-welcome}" # CHANGE ON FIRST LOGIN
+locale="${locale:-}" # set via prompt if not configured
archsetup_dir="$(dirname "$(readlink -f "$0")")"
dotfiles_dir="$archsetup_dir/dotfiles"
@@ -135,6 +141,7 @@ dmenu_repo="${dmenu_repo:-https://git.cjennings.net/dmenu.git}"
st_repo="${st_repo:-https://git.cjennings.net/st.git}"
slock_repo="${slock_repo:-https://git.cjennings.net/slock.git}"
dotemacs_repo="${dotemacs_repo:-https://git.cjennings.net/dotemacs.git}"
+archsetup_repo="${archsetup_repo:-https://git.cjennings.net/archsetup.git}"
logfile="/var/log/archsetup-$(date +'%Y-%m-%d-%H-%M-%S').log"
source_dir="/home/$username/.local/src" # aur/git source goes here
@@ -145,6 +152,12 @@ archsetup_packages="/var/log/archsetup-installed-packages.txt"
min_disk_space_gb=20
state_dir="/var/lib/archsetup/state"
error_messages=()
+errors_encountered=0
+
+# Installation steps (single source of truth for show_status and main execution)
+STEPS=(intro prerequisites create_user user_customizations aur_installer
+ essential_services xorg dwm desktop_environment developer_workstation
+ supplemental_software boot_ux)
### Cleanup Trap
# Ensures tmpfs is unmounted if script exits unexpectedly
@@ -198,10 +211,7 @@ show_status() {
exit 0
fi
echo "Completed steps:"
- for step in intro prerequisites create_user user_customizations \
- aur_installer essential_services xorg dwm \
- desktop_environment developer_workstation \
- supplemental_software boot_ux; do
+ for step in "${STEPS[@]}"; do
if step_completed "$step"; then
timestamp=$(cat "$state_dir/$step")
printf " [x] %-25s (%s)\n" "$step" "$timestamp"
@@ -265,14 +275,49 @@ preflight_checks() {
fi
echo " [OK] System: Arch Linux detected"
+ # Check locale configuration
+ if grep -q "^LANG=" /etc/locale.conf 2>/dev/null; then
+ current_locale=$(grep "^LANG=" /etc/locale.conf | cut -d= -f2)
+ echo " [OK] Locale: $current_locale"
+ elif [[ -n "$locale" ]]; then
+ echo " [OK] Locale: $locale (from config)"
+ else
+ echo ""
+ echo "Locale not configured. Please select:"
+ echo " 1) en_US.UTF-8 (US English)"
+ echo " 2) en_GB.UTF-8 (UK English)"
+ echo " 3) de_DE.UTF-8 (German)"
+ echo " 4) es_ES.UTF-8 (Spanish)"
+ echo " 5) fr_FR.UTF-8 (French)"
+ echo " 6) pt_BR.UTF-8 (Brazilian Portuguese)"
+ echo " 7) ja_JP.UTF-8 (Japanese)"
+ echo " 8) zh_CN.UTF-8 (Chinese)"
+ echo " 9) Other (enter manually)"
+ read -r -p "Choice [1]: " choice
+ case "${choice:-1}" in
+ 1) locale="en_US.UTF-8" ;;
+ 2) locale="en_GB.UTF-8" ;;
+ 3) locale="de_DE.UTF-8" ;;
+ 4) locale="es_ES.UTF-8" ;;
+ 5) locale="fr_FR.UTF-8" ;;
+ 6) locale="pt_BR.UTF-8" ;;
+ 7) locale="ja_JP.UTF-8" ;;
+ 8) locale="zh_CN.UTF-8" ;;
+ 9)
+ read -r -p "Enter locale (e.g., nl_NL.UTF-8): " locale
+ [[ -z "$locale" ]] && locale="en_US.UTF-8"
+ ;;
+ *) locale="en_US.UTF-8" ;;
+ esac
+ echo " [OK] Locale: $locale (selected)"
+ fi
+
echo "Pre-flight checks passed."
echo ""
}
### Intro
intro() {
- errors_encountered=0
-
# begin with a clean logfile
[ -f "$logfile" ] && rm -f "$logfile"
touch "$logfile"
@@ -334,28 +379,30 @@ retry_install() {
local source="$2"
local cmd="$3"
local attempt=1
+ local last_exit_code=0
display "task" "installing $pkg via $source"
while [ $attempt -le $MAX_INSTALL_RETRIES ]; do
if eval "$cmd" >> "$logfile" 2>&1; then
return 0
fi
+ last_exit_code=$?
attempt=$((attempt + 1))
if [ $attempt -le $MAX_INSTALL_RETRIES ]; then
display "task" "retrying $pkg (attempt $attempt/$MAX_INSTALL_RETRIES)"
fi
done
- error_warn "$pkg ($source)" "$?"
+ error_warn "$pkg ($source)" "$last_exit_code"
}
# Pacman Install
pacman_install() {
- retry_install "$1" "pacman" "pacman --noconfirm --needed -S $1"
+ retry_install "$1" "pacman" "pacman --noconfirm --needed -S \"$1\""
}
# AUR Install
aur_install() {
- retry_install "$1" "AUR" "sudo -u $username yay -S --noconfirm $1"
+ retry_install "$1" "AUR" "sudo -u \"$username\" yay -S --noconfirm \"$1\""
}
# Git Install
@@ -600,11 +647,19 @@ prerequisites() {
display "subtitle" "Environment Configuration"
# configure locale (must happen before package installs that depend on locale)
- action="configuring locale" && display "task" "$action"
- sed -i 's/^#en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen
- (locale-gen >> "$logfile" 2>&1) || error_warn "$action" "$?"
- echo "LANG=en_US.UTF-8" > /etc/locale.conf
- export LANG=en_US.UTF-8
+ # Skip if already configured by installer
+ if ! grep -q "^LANG=" /etc/locale.conf 2>/dev/null; then
+ action="configuring locale ($locale)" && display "task" "$action"
+ # Uncomment the selected locale in locale.gen (format: "en_US.UTF-8 UTF-8")
+ locale_entry="${locale} ${locale##*.}" # e.g., "en_US.UTF-8 UTF-8"
+ sed -i "s/^#${locale_entry}/${locale_entry}/" /etc/locale.gen
+ (locale-gen >> "$logfile" 2>&1) || error_warn "$action" "$?"
+ echo "LANG=$locale" > /etc/locale.conf
+ export LANG="$locale"
+ else
+ display "task" "locale already configured ($(grep ^LANG= /etc/locale.conf))"
+ export LANG="$(grep ^LANG= /etc/locale.conf | cut -d= -f2)"
+ fi
# sync the time on this machine (one-shot chrony sync)
action="synchronizing system time" && display "task" "$action"
@@ -663,7 +718,7 @@ EOF
[ -f /etc/sudoers.pacnew ] && cp /etc/sudoers.pacnew /etc/sudoers >> "$logfile" 2>&1
action="creating a directory to build/install software from git/AUR."
- (mkdir -p "$source_dir") || error_fatal "creating the directory $source_dir"
+ (mkdir -p "$source_dir") || error_fatal "creating the directory $source_dir" "$?"
}
@@ -674,15 +729,16 @@ create_user() {
display "task" "checking if user exists"
# halt if $username exists
( id -u "$username" >/dev/null 2>&1; ) && \
- error_fatal "user '$username' already exists!"
+ error_fatal "user '$username' already exists" "user exists"
# create $username with home, group, shell, password
action="creating user and home directory" && display "task" "$action"
(useradd -m -G wheel -s /bin/zsh "$username" >> "$logfile" 2>&1) || \
- error_fatal "adding user '$username" "$?"
+ error_fatal "adding user '$username'" "$?"
display "task" "assigning the password"
echo "$username:$password" | chpasswd # any text is allowable! be careful!
+ unset password # clear from memory after use
display "task" "adding to appropriate groups"
(usermod -aG \
@@ -726,7 +782,7 @@ user_customizations() {
user_archsetup_dir="/home/$username/code/archsetup"
action="cloning archsetup to user's home directory" && display "task" "$action"
(mkdir -p "$(dirname "$user_archsetup_dir")" && \
- git clone --depth 1 https://git.cjennings.net/archsetup.git "$user_archsetup_dir" && \
+ git clone --depth 1 "$archsetup_repo" "$user_archsetup_dir" && \
chown -R "$username": "/home/$username/code") \
>> "$logfile" 2>&1 || error_warn "$action" "$?"
@@ -741,8 +797,9 @@ user_customizations() {
pacman_install desktop-file-utils
action="updating desktop database" && display "task" "$action"
+ # Exit code 1 means warnings (missing icons, etc.) but database still updated
(sudo -u "$username" update-desktop-database "/home/$username/.local/share/applications" \
- >> "$logfile" 2>&1 ) || error_warn "$action" "$?"
+ >> "$logfile" 2>&1 ) || true
action="restoring dotfile versions" && display "task" "$action"
(cd "$dotfiles_dir" && git config --global --add safe.directory "$user_archsetup_dir" && \
@@ -828,8 +885,11 @@ ethernet.cloned-mac-address=stable
EOF
# Configure wireless regulatory domain (enables full WiFi capabilities for region)
- action="configuring wireless regulatory domain (US)" && display "task" "$action"
- sed -i 's/^#WIRELESS_REGDOM="US"/WIRELESS_REGDOM="US"/' /etc/conf.d/wireless-regdom
+ # Derive region code from locale (e.g., en_US.UTF-8 → US, de_DE.UTF-8 → DE)
+ current_lang="${LANG:-en_US.UTF-8}"
+ wireless_region="${current_lang:3:2}" # extract country code (positions 3-4)
+ action="configuring wireless regulatory domain ($wireless_region)" && display "task" "$action"
+ sed -i "s/^#WIRELESS_REGDOM=\"$wireless_region\"/WIRELESS_REGDOM=\"$wireless_region\"/" /etc/conf.d/wireless-regdom
# Encrypted DNS (DNS over TLS)
@@ -842,6 +902,8 @@ DNS=1.1.1.1#cloudflare-dns.com 9.9.9.9#dns.quad9.net
FallbackDNS=1.0.0.1#cloudflare-dns.com 149.112.112.112#dns.quad9.net
DNSOverTLS=yes
DNSSEC=yes
+# Disable mDNS in resolved - avahi handles .local resolution exclusively
+MulticastDNS=no
EOF
# Configure NetworkManager to use systemd-resolved
@@ -941,6 +1003,9 @@ EOF
systemctl enable ufw.service >> "$logfile" 2>&1 || error_warn "$action" "$?"
# Verify firewall is actually active
+ # Note: In VM environments, UFW may show inactive due to missing kernel
+ # netfilter modules. This is a test environment limitation, not a bug.
+ # On real hardware with proper kernel support, UFW activates correctly.
action="verifying firewall is active" && display "task" "$action"
if ! ufw status | grep -q "Status: active"; then
error_messages=("FIREWALL NOT ACTIVE - run: sudo ufw enable" "${error_messages[@]}")
@@ -1066,81 +1131,8 @@ EOF
use_template = none
EOF
- action="creating zfs-replicate script" && display "task" "$action"
- cat << 'EOF' > /usr/local/bin/zfs-replicate
-#!/bin/bash
-# zfs-replicate - Replicate ZFS datasets to TrueNAS
-#
-# Usage:
-# zfs-replicate # Replicate all configured datasets
-# zfs-replicate [dataset] # Replicate specific dataset
-
-set -e
-
-# TrueNAS Configuration
-# Try local network first, fall back to tailscale
-TRUENAS_LOCAL="truenas.local"
-TRUENAS_TAILSCALE="truenas"
-TRUENAS_USER="root"
-TRUENAS_POOL="vault"
-BACKUP_PATH="backups" # TODO: Configure actual path
-
-# Datasets to replicate
-DATASETS="zroot/ROOT/default zroot/home zroot/media zroot/vms"
-
-# Colors
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-RED='\033[0;31m'
-NC='\033[0m'
-
-info() { echo -e "${GREEN}[INFO]${NC} $1"; }
-warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
-error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
-
-command -v syncoid >/dev/null 2>&1 || error "syncoid not found. Install sanoid package."
-
-# Determine which host to use
-determine_host() {
- if ping -c 1 -W 2 "$TRUENAS_LOCAL" &>/dev/null; then
- echo "$TRUENAS_LOCAL"
- elif ping -c 1 -W 2 "$TRUENAS_TAILSCALE" &>/dev/null; then
- echo "$TRUENAS_TAILSCALE"
- else
- error "Cannot reach TrueNAS at $TRUENAS_LOCAL or $TRUENAS_TAILSCALE"
- fi
-}
-
-TRUENAS_HOST=$(determine_host)
-info "Using TrueNAS host: $TRUENAS_HOST"
-
-# Single dataset mode
-if [[ -n "$1" ]]; then
- dataset="$1"
- dest="$TRUENAS_USER@$TRUENAS_HOST:$TRUENAS_POOL/$BACKUP_PATH/${dataset#zroot/}"
- info "Replicating $dataset -> $dest"
- syncoid --recursive "$dataset" "$dest"
- exit 0
-fi
-
-# Full replication
-info "Starting ZFS replication to $TRUENAS_HOST"
-echo ""
-
-for dataset in $DATASETS; do
- dest="$TRUENAS_USER@$TRUENAS_HOST:$TRUENAS_POOL/$BACKUP_PATH/${dataset#zroot/}"
- info "Replicating $dataset -> $dest"
-
- if syncoid --recursive "$dataset" "$dest"; then
- info " Success"
- else
- warn " Failed (will retry next run)"
- fi
- echo ""
-done
-
-info "Replication complete."
-EOF
+ action="installing zfs-replicate script" && display "task" "$action"
+ cp "$user_archsetup_dir/scripts/zfs-replicate" /usr/local/bin/zfs-replicate
chmod +x /usr/local/bin/zfs-replicate
action="creating zfs-replicate systemd service" && display "task" "$action"
@@ -1246,7 +1238,7 @@ dwm() {
action="DWM Window Manager Dependencies" && display "subtitle" "$action"
for software in coreutils fontconfig freetype2 glibc harfbuzz libx11 libxft libxinerama; do
pacman_install "$software"
- done;
+ done
action="DWM Window Manager" && display "subtitle" "$action"
@@ -1350,7 +1342,7 @@ desktop_environment() {
for software in alsa-utils pipewire wireplumber pipewire-pulse \
pipewire-docs pamixer pulsemixer ffmpeg rtkit; do
pacman_install "$software"
- done;
+ done
# disable the pc speaker beep
rmmod pcspkr 2>/dev/null || true
echo "blacklist pcspkr" > /etc/modprobe.d/nobeep.conf
@@ -1386,11 +1378,11 @@ desktop_environment() {
for software in htop mc ncdu tmux fzf zip unzip atool wget detox \
lsof usbutils moreutils; do
pacman_install "$software"
- done;
+ done
for software in task-spooler-cpu speedtest-go-bin gotop-bin rar; do
aur_install "$software"
- done;
+ done
# Help And Documentation
@@ -1422,7 +1414,7 @@ desktop_environment() {
for software in brightnessctl network-manager-applet xclip rofi \
conky qalculate-gtk feh; do
pacman_install "$software"
- done;
+ done
aur_install caffeine-ng
aur_install flameshot
@@ -1436,12 +1428,12 @@ desktop_environment() {
for software in picom lxappearance gnome-themes-extra; do
pacman_install "$software"
- done;
+ done
for software in vimix-cursors \
papirus-icon-theme qt6ct qt5ct; do
aur_install "$software"
- done;
+ done
pacman_install libappindicator-gtk3 # required by some applets
@@ -1834,18 +1826,9 @@ outro() {
preflight_checks # verify system requirements (always runs)
STARTTIME=$(date +%s) # must be outside intro() since it may be skipped on resume
-run_step "intro" intro
-run_step "prerequisites" prerequisites
-run_step "create_user" create_user
-run_step "user_customizations" user_customizations
-run_step "aur_installer" aur_installer
-run_step "essential_services" essential_services
-run_step "xorg" xorg
-run_step "dwm" dwm
-run_step "desktop_environment" desktop_environment
-run_step "developer_workstation" developer_workstation
-run_step "supplemental_software" supplemental_software
-run_step "boot_ux" boot_ux
+for step in "${STEPS[@]}"; do
+ run_step "$step" "$step"
+done
outro # take end stats; show summary (always runs)