Files

462 lines
18 KiB
Bash

#!/bin/bash
# Bump this number whenever you push a change to GitHub, so the self-update
# check below can tell an older local copy from a newer (or unpushed) one.
SCRIPT_VERSION=2
# --- root check ---
if [[ $EUID -ne 0 ]]; then
printf "\n\e[1;31mError: This script must be run as root (use sudo).\e[0m\n\n"
exit 1
fi
# --- self-update check ---
# Compares SCRIPT_VERSION against the version on GitHub; only updates if the
# remote version is strictly higher, so an unpushed local copy that's ahead
# of GitHub is never overwritten by an older remote one.
# Set ARCH_SH_SKIP_UPDATE=1 to skip this (also set automatically after an update,
# to prevent a re-download loop).
if [[ -z "$ARCH_SH_SKIP_UPDATE" ]] && ! command -v curl &>/dev/null; then
printf "\ncurl not found - skipping self-update check.\n"
fi
if [[ -z "$ARCH_SH_SKIP_UPDATE" ]] && command -v curl &>/dev/null; then
script_url="https://raw.githubusercontent.com/georgeabr/arch/master/arch.sh?_=$(date +%s)"
script_path="$(readlink -f "$0")"
tmp_script=$(mktemp)
printf "\nChecking for a newer version of this script...\n"
if curl -fsS --max-time 5 -o "$tmp_script" "$script_url"; then
remote_version="$(grep -m1 '^SCRIPT_VERSION=' "$tmp_script" | cut -d= -f2)"
remote_version="${remote_version:-0}"
if ! [[ "$remote_version" =~ ^[0-9]+$ ]]; then
printf "Could not determine the version of the script on GitHub - skipping update to be safe.\n"
elif (( remote_version > SCRIPT_VERSION )); then
if cp "$tmp_script" "$script_path"; then
printf "A newer version is available (local: %s, GitHub: %s) - updating and restarting...\n\n" "$SCRIPT_VERSION" "$remote_version"
chmod +x "$script_path"
rm -f "$tmp_script"
export ARCH_SH_SKIP_UPDATE=1
exec "$script_path" "$@"
else
printf "A newer version is available, but the update failed (couldn't write to %s) - continuing with current version.\n" "$script_path"
fi
elif (( remote_version < SCRIPT_VERSION )); then
printf "Local version (%s) is newer than the version on GitHub (%s) - skipping update. Don't forget to push your changes.\n" "$SCRIPT_VERSION" "$remote_version"
else
printf "Script is already up to date (version %s).\n" "$SCRIPT_VERSION"
fi
else
printf "Could not check for updates (no internet or GitHub unreachable) - continuing with current version.\n"
fi
rm -f "$tmp_script" 2>/dev/null
fi
# --- start self-logging ---
timestamp=$(date +%Y%m%d_%H%M)
logfile="install-${timestamp}.log"
# redirect all output (stdout+stderr) into tee => logfile _and_ console
exec > >(tee -a "$logfile") 2>&1
# echo "==> Logging install run to $logfile"
# ----------------------------------------
# git clone https://github.com/georgeabr/arch.git
# git config --global user.email "email@gmail.com"
# git config --global user.name "georgeabr"
# to save passwords
# git config credential.helper store
# commit the code
# git add .; git commit -m "added"; git push -u origin master
# TO RUN THIS FROM AN ARCH LINUX ISO
# bash
# curl -L -o arch.sh https://raw.githubusercontent.com/georgeabr/arch/refs/heads/master/arch.sh; chmod +x arch.sh
# curl -L -o arch.sh https://bit.ly/4lDqXHQ; chmod +x arch.sh
# TO RUN THIS SCRIPT WITH LOGGING ENABLED
# ./arch.sh 2>&1 | tee install-$(date +%Y%m%d_%H%MM).log
# CONNECT VIA SSH FROM ANOTHER COMPUTER
# - set password for root user
# passwd
# - run the ssh command:
# ssh root@ip-address
# parted examples
# https://wiki.archlinux.org/index.php/Parted#UEFI/GPT_examples
# hostname="arx"
hostname="arx-$(tr -dc 'a-z' </dev/urandom | head -c3)"
username="george"
# can be ext4 or xfs
filesystem="ext4"
# New: Function to check if a value is in "X-Y" format (e.g., 1-1, 2-3)
is_disk_partition_format() {
[[ "$1" =~ ^[0-9]+-[0-9]+$ ]]
}
show_instructions() {
printf "\n\e[1mArch Linux Installer (Intel/KDE Plasma 6)\e[0m\n"
printf "Hostname: \e[1m$hostname\e[0m | User: \e[1m$username\e[0m | Filesystem: \e[1m$filesystem\e[0m\n"
printf "Usage: \e[1m$0 UEFI-PART ROOT-PART SWAP-PART\e[0m (e.g., $0 1-1 1-3 1-2)\n\n"
printf "1. Use \e[1mcfdisk\e[0m to partition your primary disk before running this.\n"
printf "2. UEFI partition should already exist; \e[1mRoot will be formatted\e[0m.\n"
printf "3. Identifiers below use 'DISK-PART' format based on /dev/nvme0n1 or /dev/sda.\n\n"
printf "Available partitions:\n"
# Create a temporary awk script for display
local awk_script_display=$(mktemp)
cat << 'EOF' > "$awk_script_display"
BEGIN {
current_disk_line = ""; current_disk_buffer = ""; has_partitions = 0; disk_counter = 0; partition_on_disk_count = 0;
# Updated exclude_regex to be more general for partition headers, accounting for potential leading spaces and variations
exclude_regex = "^\\s*(Units: sectors of|Sector size \\(|I/O size \\(|Device\\s+(Boot\\s+)?Start\\s+End\\s+Sectors\\s+Size\\s+(Id\\s+)?Type)$";
}
$0 ~ exclude_regex { next }
/^Disk \/dev\// {
if (has_partitions) { if (current_disk_buffer != "") { print current_disk_buffer; } } else { current_disk_buffer = ""; }
current_disk_line = $0; current_disk_buffer = ""; has_partitions = 0; next;
}
/^\/dev\// && !/Disklabel/ {
if (!has_partitions) {
disk_counter++; partition_on_disk_count = 0;
print current_disk_line;
if (current_disk_buffer != "") { print current_disk_buffer; }
# Removed leading newline from printf to remove blank line
printf " %-28s %-10s %-10s %-10s %-8s %s\n", "Device", "Start", "End", "Sectors", "Size", "Type";
has_partitions = 1; current_disk_buffer = "";
}
partition_on_disk_count++;
device = $1;
local_start = ""; local_end = ""; local_sectors = ""; local_size = "";
# Dynamically find the 'Size' field (e.g., 402M, 23.3G)
size_field_idx = 0;
for (k = 1; k <= NF; k++) {
if ($k ~ /^[0-9.]+(M|G|T|K|B)$/) { # Match fields like 402M, 23.3G, 18.4G
size_field_idx = k;
local_size = $k;
break;
}
}
# Deduce Start, End, Sectors based on size_field_idx relative to Device ($1)
# This assumes consistent relative positioning before Size
if (size_field_idx > 5) { # Likely DOS with Boot flag, or more complex output
local_start = $(size_field_idx - 3);
local_end = $(size_field_idx - 2);
local_sectors = $(size_field_idx - 1);
type_start_field = size_field_idx + 2; # Type after Id
} else { # Likely GPT or DOS without Boot flag
local_start = $2;
local_end = $3;
local_sectors = $4;
type_start_field = size_field_idx + 1; # Type after Size (or Id if present)
# Refine type_start_field if there's an 'Id' field after Size
if ($(size_field_idx + 1) ~ /^[0-9a-fA-F]+$/) { # Check if next field is an ID (hex/numeric)
type_start_field = size_field_idx + 2;
}
}
local_type = "";
for (j = type_start_field; j <= NF; j++) {
local_type = local_type $j (j < NF ? " " : "");
}
sub(/^ /, "", local_type); # Remove leading space if any
printf " %-28s %-10s %-10s %-10s %-8s %s\n", (disk_counter "-" partition_on_disk_count ". " device), local_start, local_end, local_sectors, local_size, local_type;
next;
}
{ # Rule for accumulating other relevant lines (like Disklabel, etc.)
if (current_disk_line != "") {
if (current_disk_buffer != "") { current_disk_buffer = current_disk_buffer "\n" $0; } else { current_disk_buffer = $0; }
}
}
END { # Handle the very last disk's output
if (has_partitions) { if (current_disk_buffer != "") { print current_disk_buffer; } }
}
EOF
# Use the temporary awk script
fdisk -l | awk -f "$awk_script_display"
rm "$awk_script_display" # Clean up the temporary file
}
start_install() {
# --- Internet check ---
printf "\nChecking for internet connectivity...\n"
if ! ping -c 1 -W 3 8.8.8.8 > /dev/null 2>&1; then
printf "\n\e[1;31mError: No internet connection detected.\e[0m\n"
printf "Please connect to the network using \`nmtui\` before running this script.\n\n"
# CLOSE the logging pipe so the terminal returns to prompt immediately
exec >&- 2>&-
# This kills the script process immediately and returns to prompt
{ sleep 0.1; kill -9 -$$; } &
exit 1
fi
printf "Internet connection verified.\n"
# -----------------------
local uefi_param="$1"
local root_param="$2"
local swap_param="$3"
# New: Parse fdisk -l to create a map of "disk_num-part_num" to actual device path
declare -A all_partitions_map # Associative array for "disk_num-part_num" => "/dev/device"
# Create a temporary awk script for parsing fdisk output
local awk_script_parse=$(mktemp)
cat << 'EOF' > "$awk_script_parse"
BEGIN {
current_disk_device_awk = "";
current_disk_num_awk = 0;
partition_num_on_disk_awk = 0;
# Updated exclude_regex_parse to be more general for partition headers
exclude_regex_parse = "^\\s*(Units: sectors of|Sector size \\(|I/O size \\(|Device\\s+.*Type)$";
}
$0 ~ exclude_regex_parse { next }
/^Disk \/dev\// {
partition_num_on_disk_awk = 0;
current_disk_device_awk = $2; sub(/:$/, "", current_disk_device_awk);
next;
}
/^\/dev\// && !/Disklabel/ {
if (current_disk_device_awk != "" && substr($1, 1, length(current_disk_device_awk)) == current_disk_device_awk) {
if (partition_num_on_disk_awk == 0) {
current_disk_num_awk++;
}
partition_num_on_disk_awk++;
print current_disk_num_awk "-" partition_num_on_disk_awk " " $1;
}
next;
}
EOF
# Capture raw fdisk output and process with the temporary awk script to build the map
local fdisk_output_raw=$(fdisk -l)
readarray -t fdisk_processed_lines < <(echo "$fdisk_output_raw" | awk -f "$awk_script_parse")
rm "$awk_script_parse" # Clean up the temporary file
# Populate the associative array in bash
for line in "${fdisk_processed_lines[@]}"; do
key=$(echo "$line" | awk '{print $1}')
value=$(echo "$line" | awk '{print $2}')
all_partitions_map["$key"]="$value"
done
# Lookup partitions using the provided parameters (e.g., 2-1, 2-3, 2-2)
uefi_part="${all_partitions_map["$uefi_param"]}"
root_part="${all_partitions_map["$root_param"]}"
swap_part="${all_partitions_map["$swap_param"]}"
# Validate if the lookups were successful
if [[ -z "$uefi_part" || -z "$root_part" || -z "$swap_part" ]]; then
printf "\n\e[1;31mError: One or more partition identifiers were invalid or not found.\e[0m\n"
printf "Please ensure the identifiers (e.g., '1-1') match available partitions.\n"
show_instructions; # Show instructions again with valid partitions
exit 1;
fi
# --- Duplicate partition check ---
if [[ "$uefi_part" == "$root_part" || "$uefi_part" == "$swap_part" || "$root_part" == "$swap_part" ]]; then
printf "\n\e[1;31mError: You cannot use the same partition for multiple roles.\e[0m\n"
printf "Your input: UEFI: $uefi_param | Root: $root_param | Swap: $swap_param\n"
exit 1
fi
# Check UEFI format
if [[ $(lsblk -no FSTYPE "$uefi_part") != "vfat" ]]; then
printf "\n\e[1;31mError: UEFI partition is not FAT32.\e[0m\n"
exec >&- 2>&-
{ sleep 0.1; kill -9 -$$; } &
exit 1
fi
printf "\nThe Arch install script will use the settings:\n";
printf "%s\n" "* host name = $hostname";
printf "%s\n" "* user name = $username";
printf "%s\n" "* filesystem = $filesystem";
printf "\nThe Arch install script will use the below partitions:\
\n* $uefi_part for UEFI \t(keep existing data for dual boot with Windows)"
printf "\n* $root_part for root (/) \t(partition will be formatted)"
printf "\n* $swap_part for swap \t(partition will be formatted if not already)\n"
printf "\n"
# Simplified display for chosen partitions within start_install
printf "\t\t\t Device\t\tSize\t\tType\n"
local parts_to_display=("$uefi_part" "$root_part" "$swap_part")
local labels=("* UEFI partition" "* Root (/) partition" "* Swap partition")
# Create a temporary awk script for displaying selected partitions
local awk_script_selected_display=$(mktemp)
cat << 'EOF' > "$awk_script_selected_display"
# No BEGIN block needed here for -v variables
/^\/dev\// {
# p_dev_awk_var is directly available from the -v flag
if ($1 == p_dev_awk_var) {
local_size = "";
local_type = "";
# Dynamically find the 'Size' field (e.g., 402M, 23.3G)
size_field_idx = 0;
for (k = 1; k <= NF; k++) {
if ($k ~ /^[0-9.]+(M|G|T|K|B)$/) { # Match fields like 402M, 23.3G, 18.4G
size_field_idx = k;
local_size = $k;
break;
}
}
# Deduce where 'Type' starts based on size_field_idx
type_start_field = size_field_idx + 1; # Default: Type starts right after Size
# If the field *after* Size is an ID (hex/numeric), then Type starts two fields after Size
if ($(size_field_idx + 1) ~ /^[0-9a-fA-F]+$/ || $(size_field_idx + 1) ~ /^[0-9]+$/) {
type_start_field = size_field_idx + 2;
}
for (j=type_start_field; j<=NF; ++j) local_type = local_type $j (j<NF ? " " : "");
sub(/^ /, "", local_type); # Remove leading space from type
printf "%-20s\t%-8s\t%s\n", $1, local_size, local_type;
exit;
}
}
EOF
for i in "${!parts_to_display[@]}"; do
local part_dev="${parts_to_display[$i]}"
local label="${labels[$i]}"
printf "%s \t = " "$label"
# Pass p_dev as an argument to awk using the -v flag
fdisk -l | awk -v p_dev_awk_var="$part_dev" -f "$awk_script_selected_display"
done
rm "$awk_script_selected_display" # Clean up the temporary file
printf "\n"
read -p "Do you wish to continue? (Y\y to continue, any other input to stop): " response
if ! [[ "$response" == "y" ]] && ! [[ "$response" == "Y" ]]; then
printf "\nExiting script.\n"
exit 1
fi
printf "\n\nWill continue to installing Arch Linux.\n"
printf "Using UK mirrors\n"
printf "\nAdding mirrors...\n"
curl -s "https://archlinux.org/mirrorlist/?country=GB&protocol=http&protocol=https&use_mirror_status=on" \
| sed -E 's/^#(Server.*)/\1/' \
> /etc/pacman.d/mirrorlist
printf "\nPart 1 - Initial Arch bootstrap/installation.\n";
printf "\nActivating swap partition.\n"
swapon "$swap_part" > /dev/null 2>&1;
if [[ $? -ne 0 ]]; then
printf "Formatting and activating swap file.\n";
mkswap "$swap_part" > /dev/null 2>&1;
swapon "$swap_part" > /dev/null 2>&1;
if [[ $? -ne 0 ]]; then
printf "\n\e[1;31mError: Failed to activate swap partition $swap_part.\e[0m\n"
exit 1
fi
fi
printf "Swap file has been enabled.\n"
case "$filesystem" in
ext4)
printf "\nFormatting root (/) partition as ext4.\n";
mkfs.ext4 -F "$root_part" > /dev/null 2>&1;
;;
xfs)
printf "\nFormatting root (/) partition as xfs.\n";
mkfs.xfs -f "$root_part" > /dev/null 2>&1;
;;
esac
if [[ $? -ne 0 ]]; then
printf "\n\e[1;31mError: Failed to format root partition $root_part.\e[0m\n"
exit 1
fi
printf "\nMounting UEFI, root (/) partitions.\n"
mount "$root_part" /mnt
if [[ $? -ne 0 ]]; then
printf "\n\e[1;31mError: Failed to mount root partition $root_part on /mnt.\e[0m\n"
exit 1
fi
mkdir -p /mnt/boot/EFI
mount "$uefi_part" /mnt/boot/EFI
if [[ $? -ne 0 ]]; then
printf "\n\e[1;31mError: Failed to mount UEFI partition $uefi_part on /mnt/boot/EFI.\e[0m\n"
exit 1
fi
printf "\nSetting systemd NTP clock sync.\n"
timedatectl set-ntp true
printf "\nInstalling base Arch packages.\n"
pacstrap /mnt linux linux-headers base base-devel linux-firmware intel-ucode bash xfsprogs
if [[ $? -ne 0 ]]; then
printf "\n\e[1;31mError: pacstrap failed to install the base system.\e[0m\n"
exit 1
fi
printf "\nCreating fstab with root/swap/UEFI.\n"
genfstab -U /mnt >> /mnt/etc/fstab
if [[ $? -ne 0 ]]; then
printf "\n\e[1;31mError: genfstab failed to generate /etc/fstab.\e[0m\n"
exit 1
fi
printf "\nChrooting into installation.\n"
# This disables account locking by setting deny to 0 in faillock.conf
arch-chroot /mnt /bin/bash -c "sed -i 's/^#\?deny =.*/deny = 0/' /etc/security/faillock.conf"
if [[ $? -ne 0 ]]; then
printf "\n\e[1;31mError: Failed to chroot into /mnt to update faillock.conf.\e[0m\n"
exit 1
fi
curl -s https://raw.githubusercontent.com/georgeabr/arch/master/arch-2.sh > arch-2.sh; \
chmod +x arch-2.sh; cp ./arch-2.sh /mnt; arch-chroot /mnt /bin/bash -c "./arch-2.sh '$hostname' '$username'";
if [[ $? -ne 0 ]]; then
printf "\n\e[1;31mError: arch-2.sh failed inside the chroot.\e[0m\n"
exit 1
fi
# Delete after chroot exits
rm -f /mnt/arch-2.sh
echo "Unmounting all filesystems under /mnt..."
umount -R /mnt
}
# This is the entry point for the script, validating parameters
if is_disk_partition_format "$1" && is_disk_partition_format "$2" && is_disk_partition_format "$3";
then
start_install "$1" "$2" "$3";
else
printf "\n\e[1;31mError: Missing or invalid arguments.\e[0m\n"
arg_labels=("UEFI-PART" "ROOT-PART" "SWAP-PART")
for i in 1 2 3; do
arg_val="${!i}"
if [[ -z "$arg_val" ]]; then
printf " - %-10s not provided\n" "${arg_labels[$((i-1))]}"
elif ! is_disk_partition_format "$arg_val"; then
printf " - %-10s '%s' is not in DISK-PART format (e.g. '1-1')\n" "${arg_labels[$((i-1))]}" "$arg_val"
fi
done
printf "\n"
show_instructions;
exit 1;
fi