Refactored installer into modular library structure with improved error handling and logging.
The changes include: - Split monolithic script into lib/, config/, profiles/, and files/ directories - Added error handling with cleanup on failure - Added installation logging to /var/log/arch-install.log - Added username validation
This commit is contained in:
222
lib/core/common.sh
Normal file
222
lib/core/common.sh
Normal file
@@ -0,0 +1,222 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2026 Logan Fick
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# common.sh - Core utility functions for messaging and user interaction
|
||||
#
|
||||
# Provides colored output, user prompts, and progress tracking used throughout
|
||||
# the installer.
|
||||
|
||||
# Color codes for terminal output
|
||||
readonly COLOR_RED='\033[0;31m'
|
||||
readonly COLOR_GREEN='\033[0;32m'
|
||||
readonly COLOR_YELLOW='\033[0;33m'
|
||||
readonly COLOR_BLUE='\033[0;34m'
|
||||
readonly COLOR_CYAN='\033[0;36m'
|
||||
readonly COLOR_BG_GRAY='\033[48;5;236m'
|
||||
readonly COLOR_RESET='\033[0m'
|
||||
|
||||
# Current installation phase (set by set_phase)
|
||||
CURRENT_PHASE=""
|
||||
|
||||
# Step counter for progress indicator
|
||||
CURRENT_STEP=0
|
||||
TOTAL_STEPS=10
|
||||
|
||||
# Print the installer banner (call once at start)
|
||||
print_banner() {
|
||||
echo ""
|
||||
echo -e "${COLOR_BLUE}:: ${INSTALLER_NAME} ::${COLOR_RESET}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Print a standard message with arrow prefix
|
||||
print() {
|
||||
echo -e "${COLOR_BLUE}→${COLOR_RESET} $1"
|
||||
}
|
||||
|
||||
# Print an informational message
|
||||
print_info() {
|
||||
echo -e "${COLOR_CYAN}ℹ${COLOR_RESET} $1"
|
||||
}
|
||||
|
||||
# Run a command with gray background for its output
|
||||
# Use this for commands that produce visible output (fdisk, pacstrap, pacman, etc.)
|
||||
run_visible_cmd() {
|
||||
echo -ne "${COLOR_BG_GRAY}"
|
||||
"$@"
|
||||
local exit_code=$?
|
||||
echo -e "${COLOR_RESET}"
|
||||
return $exit_code
|
||||
}
|
||||
|
||||
# Print an installation step/phase header with progress indicator
|
||||
print_step() {
|
||||
local step="$1"
|
||||
CURRENT_STEP=$((CURRENT_STEP + 1))
|
||||
CURRENT_PHASE="$step"
|
||||
echo ""
|
||||
echo -e "${COLOR_BLUE}=== [${CURRENT_STEP}/${TOTAL_STEPS}] ${step} ===${COLOR_RESET}"
|
||||
}
|
||||
|
||||
# Print a success message
|
||||
print_success() {
|
||||
echo -e "${COLOR_GREEN}[OK]${COLOR_RESET} $1"
|
||||
}
|
||||
|
||||
# Print a warning message
|
||||
print_warning() {
|
||||
echo -e "${COLOR_YELLOW}[WARNING]${COLOR_RESET} $1"
|
||||
}
|
||||
|
||||
# Print an error message
|
||||
print_error() {
|
||||
echo -e "${COLOR_RED}[ERROR]${COLOR_RESET} $1" >&2
|
||||
}
|
||||
|
||||
# Ask for yes/no confirmation
|
||||
# Arguments:
|
||||
# $1 - prompt message
|
||||
# Returns:
|
||||
# 0 if user confirms, 1 otherwise
|
||||
confirm() {
|
||||
local prompt="$1"
|
||||
local response
|
||||
|
||||
print "${prompt} [y/N]: "
|
||||
read -r response
|
||||
|
||||
case "$response" in
|
||||
[yY][eE][sS]|[yY])
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Require "I am sure" confirmation for destructive operations
|
||||
# Arguments:
|
||||
# $1 - warning message
|
||||
# Returns:
|
||||
# 0 if user confirms, 1 otherwise
|
||||
require_confirmation() {
|
||||
local warning="$1"
|
||||
local response
|
||||
|
||||
print "${warning}"
|
||||
print "Enter 'I am sure' exactly to confirm, or anything else to cancel."
|
||||
read -r response
|
||||
|
||||
if [ "$response" = "I am sure" ]; then
|
||||
return 0
|
||||
else
|
||||
print "Confirmation failed. Exiting..."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Prompt for user input
|
||||
# Arguments:
|
||||
# $1 - prompt message
|
||||
# $2 - variable name to store result
|
||||
prompt() {
|
||||
local prompt_msg="$1"
|
||||
local var_name="$2"
|
||||
local response
|
||||
|
||||
print "$prompt_msg"
|
||||
read -r response
|
||||
eval "$var_name='$response'"
|
||||
}
|
||||
|
||||
# Prompt for secret input (no echo)
|
||||
# Arguments:
|
||||
# $1 - prompt message
|
||||
# $2 - variable name to store result
|
||||
prompt_secret() {
|
||||
local prompt_msg="$1"
|
||||
local var_name="$2"
|
||||
local response
|
||||
|
||||
print "$prompt_msg"
|
||||
read -rs response
|
||||
echo
|
||||
eval "$var_name='$response'"
|
||||
}
|
||||
|
||||
# Prompt for password with confirmation
|
||||
# Arguments:
|
||||
# $1 - prompt message
|
||||
# $2 - variable name to store result
|
||||
# Returns:
|
||||
# 0 on success, 1 if passwords don't match
|
||||
prompt_password() {
|
||||
local prompt_msg="$1"
|
||||
local var_name="$2"
|
||||
local password
|
||||
local password_confirm
|
||||
|
||||
print "$prompt_msg"
|
||||
read -rs password
|
||||
echo
|
||||
|
||||
print "Please confirm your password."
|
||||
read -rs password_confirm
|
||||
echo
|
||||
|
||||
if [ "$password" != "$password_confirm" ]; then
|
||||
print_error "Passwords do not match."
|
||||
return 1
|
||||
fi
|
||||
|
||||
eval "$var_name='$password'"
|
||||
unset password password_confirm
|
||||
return 0
|
||||
}
|
||||
|
||||
# Display a menu and get user selection
|
||||
# Arguments:
|
||||
# $1 - menu title
|
||||
# $@ - menu options (remaining arguments)
|
||||
# Returns:
|
||||
# Selected option number in MENU_SELECTION variable
|
||||
prompt_menu() {
|
||||
local title="$1"
|
||||
shift
|
||||
local options=("$@")
|
||||
local i=1
|
||||
|
||||
print "$title"
|
||||
for option in "${options[@]}"; do
|
||||
print " $i - $option"
|
||||
((i++))
|
||||
done
|
||||
|
||||
read -r MENU_SELECTION
|
||||
}
|
||||
|
||||
# Wait for user to press enter
|
||||
wait_for_enter() {
|
||||
local message="${1:-Press enter to continue.}"
|
||||
print "$message"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Get the directory containing the main script
|
||||
get_script_dir() {
|
||||
echo "$SCRIPT_DIR"
|
||||
}
|
||||
124
lib/core/error.sh
Normal file
124
lib/core/error.sh
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2026 Logan Fick
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# error.sh - Error handling framework
|
||||
#
|
||||
# Implements robust error handling for the installation process:
|
||||
# - Sets up bash strict mode (set -e) and ERR trap
|
||||
# - Provides detailed error messages with phase, line number, and failed command
|
||||
# - Offers automatic cleanup on failure (unmount filesystems, close LUKS)
|
||||
# - Includes retry helper for transient failures
|
||||
|
||||
# Enable strict error handling
|
||||
set -e
|
||||
|
||||
# Set up error trap
|
||||
trap_errors() {
|
||||
trap 'handle_error $? $LINENO "$BASH_COMMAND"' ERR
|
||||
}
|
||||
|
||||
# Error handler function
|
||||
# Arguments:
|
||||
# $1 - exit code
|
||||
# $2 - line number
|
||||
# $3 - failed command
|
||||
handle_error() {
|
||||
local exit_code=$1
|
||||
local line_number=$2
|
||||
local command="$3"
|
||||
|
||||
echo ""
|
||||
print_error "Installation failed!"
|
||||
print_error "Phase: ${CURRENT_PHASE:-unknown}"
|
||||
print_error "Exit code: $exit_code at line $line_number"
|
||||
print_error "Command: $command"
|
||||
echo ""
|
||||
|
||||
# Offer cleanup
|
||||
print "Would you like to attempt cleanup? [y/N]: "
|
||||
read -r response
|
||||
|
||||
case "$response" in
|
||||
[yY][eE][sS]|[yY])
|
||||
cleanup_on_error
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Cleanup function for error recovery
|
||||
cleanup_on_error() {
|
||||
print_warning "Cleaning up after error..."
|
||||
|
||||
# Unmount filesystems (ignore errors)
|
||||
umount -R "${MOUNT_POINT}" 2>/dev/null || true
|
||||
|
||||
# Close LUKS containers (ignore errors)
|
||||
cryptsetup close cryptroot 2>/dev/null || true
|
||||
cryptsetup close cryptroot-primary 2>/dev/null || true
|
||||
cryptsetup close cryptroot-secondary 2>/dev/null || true
|
||||
|
||||
print "Cleanup complete. You may retry the installation."
|
||||
}
|
||||
|
||||
# Set the current installation phase for better error messages
|
||||
# Arguments:
|
||||
# $1 - phase name
|
||||
set_phase() {
|
||||
CURRENT_PHASE="$1"
|
||||
print_step "$1"
|
||||
}
|
||||
|
||||
# Run a command with error context
|
||||
# Arguments:
|
||||
# $1 - description
|
||||
# $@ - command and arguments
|
||||
safe_run() {
|
||||
local description="$1"
|
||||
shift
|
||||
|
||||
print " $description..."
|
||||
if ! "$@"; then
|
||||
print_error "Failed: $description"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Retry a command with exponential backoff
|
||||
# Arguments:
|
||||
# $1 - max attempts
|
||||
# $2 - initial delay in seconds
|
||||
# $@ - command and arguments
|
||||
retry() {
|
||||
local max_attempts="$1"
|
||||
local delay="$2"
|
||||
shift 2
|
||||
|
||||
local attempt=1
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if "$@"; then
|
||||
return 0
|
||||
fi
|
||||
print_warning "Attempt $attempt/$max_attempts failed. Retrying in ${delay}s..."
|
||||
sleep "$delay"
|
||||
((attempt++))
|
||||
((delay *= 2))
|
||||
done
|
||||
|
||||
print_error "All $max_attempts attempts failed."
|
||||
return 1
|
||||
}
|
||||
71
lib/core/logging.sh
Normal file
71
lib/core/logging.sh
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2026 Logan Fick
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# logging.sh - Installation logging
|
||||
#
|
||||
# Captures the complete installation session for troubleshooting:
|
||||
# - Redirects all stdout/stderr to both console and log file
|
||||
# - Records timestamps at start and end of installation
|
||||
# - Includes git commit hash for version tracking
|
||||
# - Copies final log to /var/log/arch-install.log on installed system
|
||||
|
||||
# Temp location during installation (installed system's /var/log doesn't exist yet)
|
||||
LOG_FILE_TEMP="/tmp/arch-install.log"
|
||||
# Final location on the installed system
|
||||
LOG_FILE="/var/log/arch-install.log"
|
||||
GITEA_URL="https://git.logal.dev/LogalDeveloper/Arch-Linux-Installer"
|
||||
|
||||
# Print installer URL with commit if available
|
||||
print_installer_url() {
|
||||
if command -v git &>/dev/null && git -C "$SCRIPT_DIR" rev-parse --git-dir &>/dev/null 2>&1; then
|
||||
local commit
|
||||
commit=$(git -C "$SCRIPT_DIR" rev-parse HEAD)
|
||||
echo "=== Installer: ${GITEA_URL}/commit/${commit} ==="
|
||||
else
|
||||
echo "=== Installer: ${GITEA_URL} ==="
|
||||
fi
|
||||
}
|
||||
|
||||
# Initialize logging - tee all output to log file
|
||||
init_logging() {
|
||||
# Create log file with secure permissions
|
||||
touch "$LOG_FILE_TEMP"
|
||||
chown root:root "$LOG_FILE_TEMP"
|
||||
chmod 640 "$LOG_FILE_TEMP"
|
||||
|
||||
# Redirect stdout and stderr to both console and log
|
||||
exec > >(tee -a "$LOG_FILE_TEMP") 2>&1
|
||||
|
||||
# Write log header
|
||||
echo "=== Installation started at $(date) ==="
|
||||
print_installer_url
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Copy log file to installed system
|
||||
finalize_logging() {
|
||||
local final_log="${MOUNT_POINT}${LOG_FILE}"
|
||||
|
||||
# Write log footer
|
||||
echo ""
|
||||
echo "=== Installation finished at $(date) ==="
|
||||
print_installer_url
|
||||
echo "=== Log saved to: ${LOG_FILE} ==="
|
||||
|
||||
cp "$LOG_FILE_TEMP" "$final_log"
|
||||
chown root:root "$final_log"
|
||||
chmod 640 "$final_log"
|
||||
}
|
||||
138
lib/core/validation.sh
Normal file
138
lib/core/validation.sh
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2026 Logan Fick
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# validation.sh - Input validation functions
|
||||
#
|
||||
# Validates user input (disks, usernames, menu selections) to prevent errors
|
||||
# during installation.
|
||||
|
||||
# Validate that a disk exists
|
||||
# Arguments:
|
||||
# $1 - disk path (e.g., /dev/sda)
|
||||
# Returns:
|
||||
# 0 if disk exists, 1 otherwise
|
||||
validate_disk_exists() {
|
||||
local disk="$1"
|
||||
|
||||
if [ ! -b "$disk" ]; then
|
||||
print_error "Disk '$disk' does not exist or is not a block device."
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Validate that a disk is not mounted
|
||||
# Arguments:
|
||||
# $1 - disk path
|
||||
# Returns:
|
||||
# 0 if not mounted, 1 if mounted
|
||||
validate_disk_not_mounted() {
|
||||
local disk="$1"
|
||||
|
||||
if mount | grep -q "^${disk}"; then
|
||||
print_error "Disk '$disk' appears to be mounted. Please unmount it first."
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Validate that two disks are different (for RAID1)
|
||||
# Arguments:
|
||||
# $1 - first disk path
|
||||
# $2 - second disk path
|
||||
# Returns:
|
||||
# 0 if different, 1 if same
|
||||
validate_disks_different() {
|
||||
local disk1="$1"
|
||||
local disk2="$2"
|
||||
|
||||
if [ "$disk1" = "$disk2" ]; then
|
||||
print_error "Both disks must be different. You entered the same disk twice."
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Validate username format
|
||||
# Arguments:
|
||||
# $1 - username
|
||||
# Returns:
|
||||
# 0 if valid, 1 otherwise
|
||||
validate_username() {
|
||||
local username="$1"
|
||||
|
||||
# Check if empty
|
||||
if [ -z "$username" ]; then
|
||||
print_error "Username cannot be empty."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check length (max 32 chars)
|
||||
if [ ${#username} -gt 32 ]; then
|
||||
print_error "Username must be 32 characters or less."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check format (lowercase letters, digits, underscore, hyphen; must start with letter)
|
||||
if ! [[ "$username" =~ ^[a-z][a-z0-9_-]*$ ]]; then
|
||||
print_error "Username must start with a lowercase letter and contain only lowercase letters, digits, underscores, and hyphens."
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Validate menu selection is in range
|
||||
# Arguments:
|
||||
# $1 - selection
|
||||
# $2 - minimum value
|
||||
# $3 - maximum value
|
||||
# Returns:
|
||||
# 0 if valid, 1 otherwise
|
||||
validate_menu_selection() {
|
||||
local selection="$1"
|
||||
local min="$2"
|
||||
local max="$3"
|
||||
|
||||
# Check if it's a number
|
||||
if ! [[ "$selection" =~ ^[0-9]+$ ]]; then
|
||||
print_error "Please enter a number between $min and $max."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check range
|
||||
if [ "$selection" -lt "$min" ] || [ "$selection" -gt "$max" ]; then
|
||||
print_error "Please enter a number between $min and $max."
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Validate password is not empty
|
||||
# Arguments:
|
||||
# $1 - password
|
||||
# Returns:
|
||||
# 0 if valid, 1 otherwise
|
||||
validate_password_not_empty() {
|
||||
local password="$1"
|
||||
|
||||
if [ -z "$password" ]; then
|
||||
print_error "Password cannot be empty."
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user