The end-4 step's chowns ran on the live medium, whose /etc/passwd has no $USERNAME (the user exists only inside /mnt), so 'chown -R alvin:alvin' failed with "invalid user" and the ERR trap aborted the install at line 543. Resolve uid/gid once via arch-chroot id and chown by number; the three remaining name-based chowns (fork/fish/hypr restores) would have hit the same wall later. Replace those three mid-function chowns with one catch-all over the target home after the last root-side write: sed -i on the presets rewrites through a temp file, which leaves them root-owned with no chown after it. systemctl --user enable ydotool.service could never work inside the chroot (no /run/user/UID/bus exists before boot; it only ever warned with "Failed to connect to user scope bus", so ydotool never autostarted). Write the symlink enable would write: ~/.config/systemd/user/default.target.wants/ydotool.service -> /usr/lib/systemd/user/ydotool.service.
807 lines
27 KiB
Bash
Executable File
807 lines
27 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# alvin-install — Gum TUI Arch Linux installer with LUKS encryption
|
|
# shellcheck disable=SC2034
|
|
|
|
set -Eeuo pipefail
|
|
|
|
readonly LOG=/var/log/alvin-install.log
|
|
readonly PASS_MIN=8
|
|
readonly ESP_SIZE="1GiB"
|
|
readonly TARGET="/mnt"
|
|
readonly -a PKGS_EXCLUDE=(archiso mkinitcpio-archiso mkinitcpio-nfs-utils syslinux memtest86+)
|
|
|
|
skip() { local x; for x in "${PKGS_EXCLUDE[@]}"; do [[ "$x" == "$1" ]] && return 0; done; return 1; }
|
|
|
|
DISK=""
|
|
KEYMAP=""
|
|
USERNAME=""
|
|
PASS=""
|
|
HOSTNAME=""
|
|
GIT_NAME=""
|
|
GIT_EMAIL=""
|
|
TIMEZONE=""
|
|
LOCALE=""
|
|
LUKS="true"
|
|
FILESYSTEM="ext4"
|
|
|
|
log() { echo "[$(date '+%H:%M:%S')] $*" >> "$LOG"; }
|
|
|
|
cleanup() {
|
|
set +e
|
|
rm -f "${TARGET}/etc/sudoers.d/99-end4-install" 2>/dev/null
|
|
umount -R "$TARGET" 2>/dev/null
|
|
cryptsetup close cryptroot 2>/dev/null
|
|
true
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'gum style --foreground 9 --bold "Failed at line $LINENO. See $LOG"' ERR
|
|
|
|
(( EUID == 0 )) || { echo "Run as root" >&2; exit 1; }
|
|
|
|
die() { gum style --foreground 9 --bold "ERROR: $*"; exit 1; }
|
|
info() { gum style --foreground 12 --bold "$*"; }
|
|
ok() { gum style --foreground 10 --bold "$*"; }
|
|
warn() { gum style --foreground 11 --bold "$*"; }
|
|
|
|
header() {
|
|
gum style \
|
|
--foreground "#FAFAFA" --background "#6C91BF" \
|
|
--padding "1 2" --bold --align center --width 70 \
|
|
"$1"
|
|
}
|
|
|
|
divider() {
|
|
gum style --foreground 8 "──────────────────────────────────────────────────────────"
|
|
}
|
|
|
|
part() { [[ "$DISK" =~ [0-9]$ ]] && echo "${DISK}p$1" || echo "${DISK}$1"; }
|
|
|
|
valid_username() { [[ "$1" =~ ^[a-z_][a-z0-9_-]*$ ]] && (( ${#1} <= 32 )); }
|
|
|
|
check_prereqs() {
|
|
command -v gum >/dev/null 2>&1 || die "gum is not installed"
|
|
[[ -d /sys/firmware/efi ]] || die "UEFI not detected. This installer requires UEFI boot."
|
|
}
|
|
|
|
# ── Screens ───────────────────────────────────────────────────────────────────
|
|
|
|
screen_welcome() {
|
|
clear
|
|
header "Alvin Arch Linux Installer"
|
|
echo ""
|
|
gum style --foreground 15 --align center \
|
|
"This will install Arch Linux with:" \
|
|
" * LUKS disk encryption (default)" \
|
|
" * GRUB bootloader" \
|
|
" * NetworkManager" \
|
|
" * Your user + git config" \
|
|
""
|
|
divider
|
|
echo ""
|
|
gum confirm --affirmative "Begin installation" --negative "Abort" \
|
|
"Ready to begin?" || die "Installation aborted by user."
|
|
}
|
|
|
|
screen_keymap() {
|
|
clear
|
|
header "Keyboard Layout"
|
|
echo ""
|
|
gum style --foreground 15 \
|
|
"Select the keyboard layout of your physical keyboard." \
|
|
"This affects passwords and the LUKS passphrase at boot." \
|
|
"" \
|
|
"The default is the US layout (us)."
|
|
echo ""
|
|
if gum confirm --affirmative "Use us (default)" --negative "Choose another layout" \
|
|
"Use US keyboard layout (us)?"; then
|
|
KEYMAP="us"
|
|
else
|
|
KEYMAP=$(localectl list-keymaps | gum filter --header "+ Keymap" --placeholder "Search keymaps...") \
|
|
|| die "No keymap selected."
|
|
fi
|
|
[[ -z "$KEYMAP" ]] && die "No keymap selected."
|
|
loadkeys "$KEYMAP" 2>/dev/null || true
|
|
log "Keymap: $KEYMAP"
|
|
}
|
|
|
|
screen_network() {
|
|
clear
|
|
header "Network Setup"
|
|
echo ""
|
|
gum style --foreground 15 --align center \
|
|
"A network connection is required for installation." \
|
|
"" \
|
|
"When nmtui opens:" \
|
|
" 1. Select 'Activate a connection'" \
|
|
" 2. Use arrow keys to pick your WiFi network" \
|
|
" 3. Enter the password and press Enter" \
|
|
" 4. Press Esc to exit nmtui"
|
|
echo ""
|
|
divider
|
|
echo ""
|
|
gum confirm --affirmative "Open nmtui" --negative "Skip (already connected)" \
|
|
"Open NetworkManager TUI?" && nmtui || true
|
|
echo ""
|
|
info "Verifying network connection..."
|
|
local try=0
|
|
while ! ping -c1 -W3 archlinux.org &>/dev/null; do
|
|
try=$((try + 1))
|
|
if (( try >= 5 )); then
|
|
gum confirm --affirmative "Retry" --negative "Abort" \
|
|
"Network check failed. Retry?" || die "No network connection."
|
|
try=0
|
|
fi
|
|
sleep 2
|
|
done
|
|
ok "Network verified."
|
|
log "Network: connected"
|
|
}
|
|
|
|
screen_disk() {
|
|
clear
|
|
header "Select Installation Disk"
|
|
echo ""
|
|
|
|
local live_dev=""
|
|
if [[ -d /run/archiso/bootmnt ]]; then
|
|
local src
|
|
src=$(findmnt -no SOURCE /run/archiso/bootmnt 2>/dev/null || true)
|
|
[[ -n "$src" ]] && live_dev="/dev/$(lsblk -no PKNAME "$src" 2>/dev/null | head -n1)"
|
|
fi
|
|
|
|
local disks=()
|
|
while IFS= read -r line; do
|
|
local name size model
|
|
name=$(echo "$line" | awk '{print $1}')
|
|
size=$(echo "$line" | awk '{print $2}')
|
|
model=$(echo "$line" | awk '{$1=$2=""; sub(/^[ \t]+/,""); print}')
|
|
[[ "$name" == "$live_dev" ]] && continue
|
|
disks+=("${name} ${size} ${model}")
|
|
done < <(lsblk -dpno NAME,SIZE,MODEL,TYPE | awk '$NF=="disk" {print $1,$2,$3}')
|
|
|
|
(( ${#disks[@]} > 0 )) || die "No disks found."
|
|
|
|
while true; do
|
|
local choice
|
|
choice=$(gum choose --header "+ Target disk (live USB hidden)" "${disks[@]}") \
|
|
|| die "No disk selected."
|
|
DISK=$(echo "$choice" | awk '{print $1}')
|
|
|
|
gum confirm --affirmative "Wipe ${DISK}" --negative "Go back" \
|
|
"WARNING: ALL data on ${DISK} will be destroyed!" && break
|
|
done
|
|
|
|
log "Disk: $DISK"
|
|
}
|
|
|
|
screen_luks() {
|
|
clear
|
|
header "Disk Encryption"
|
|
echo ""
|
|
gum style --foreground 15 --align center \
|
|
"LUKS encrypts your root filesystem." \
|
|
"The passphrase is the same as your login password." \
|
|
"Your data stays protected if the device is lost or stolen."
|
|
echo ""
|
|
echo ""
|
|
if gum confirm --affirmative "Enable LUKS encryption" --negative "No encryption" \
|
|
"Enable LUKS disk encryption? (default: yes)"; then
|
|
LUKS="true"
|
|
else
|
|
LUKS="false"
|
|
fi
|
|
log "LUKS: $LUKS"
|
|
}
|
|
|
|
screen_filesystem() {
|
|
clear
|
|
header "Filesystem"
|
|
echo ""
|
|
FILESYSTEM=$(gum choose --header "+ Root filesystem" "ext4" "btrfs") \
|
|
|| die "No filesystem selected."
|
|
log "Filesystem: $FILESYSTEM"
|
|
}
|
|
|
|
screen_username() {
|
|
clear
|
|
header "Create User"
|
|
echo ""
|
|
local input
|
|
input=$(gum input --placeholder "username" --char-limit 32 --header "+ Username") \
|
|
|| die "No username entered."
|
|
[[ -z "$input" ]] && die "Username cannot be empty."
|
|
valid_username "$input" || die "Invalid username. Use lowercase letters, digits, _ or -."
|
|
USERNAME="$input"
|
|
log "Username: $USERNAME"
|
|
}
|
|
|
|
screen_password() {
|
|
clear
|
|
header "Set Password"
|
|
echo ""
|
|
gum style --foreground 15 \
|
|
"This password will be used for login and LUKS disk encryption."
|
|
echo ""
|
|
|
|
local pass1 pass2
|
|
pass1=$(gum input --password --header "+ Password") || die "No password entered."
|
|
(( ${#pass1} >= PASS_MIN )) || die "Password must be at least ${PASS_MIN} characters."
|
|
pass2=$(gum input --password --header "+ Confirm password") || die "No confirmation entered."
|
|
[[ "$pass1" == "$pass2" ]] || die "Passwords do not match."
|
|
PASS="$pass1"
|
|
log "Password: set (${#PASS} chars)"
|
|
}
|
|
|
|
screen_hostname() {
|
|
clear
|
|
header "Hostname"
|
|
echo ""
|
|
HOSTNAME=$(gum input --placeholder "archlinux" --char-limit 64 --header "+ Hostname") \
|
|
|| die "No hostname entered."
|
|
[[ -z "$HOSTNAME" ]] && HOSTNAME="archlinux"
|
|
log "Hostname: $HOSTNAME"
|
|
}
|
|
|
|
screen_git() {
|
|
clear
|
|
header "Git Configuration"
|
|
echo ""
|
|
GIT_NAME=$(gum input --value "$USERNAME" --header "+ Git name") \
|
|
|| die "No git name entered."
|
|
GIT_EMAIL=$(gum input --placeholder "user@example.com" --header "+ Git email") \
|
|
|| die "No git email entered."
|
|
[[ -z "$GIT_EMAIL" ]] && die "Git email cannot be empty."
|
|
log "Git: $GIT_NAME <$GIT_EMAIL>"
|
|
}
|
|
|
|
screen_timezone() {
|
|
clear
|
|
header "Timezone"
|
|
echo ""
|
|
TIMEZONE=$(timedatectl list-timezones | gum filter --header "+ Timezone" --placeholder "Search...") \
|
|
|| die "No timezone selected."
|
|
log "Timezone: $TIMEZONE"
|
|
}
|
|
|
|
screen_locale() {
|
|
clear
|
|
header "Locale"
|
|
echo ""
|
|
local locales
|
|
locales=$(sed -n 's/^#\?\([a-z][a-z]*_[A-Z][A-Z]\.UTF-8\) UTF-8.*/\1/p' /etc/locale.gen | sort -u)
|
|
[[ -z "$locales" ]] && locales="en_US.UTF-8"
|
|
LOCALE=$(echo "$locales" | gum filter --header "+ Locale" --placeholder "Search...") \
|
|
|| die "No locale selected."
|
|
log "Locale: $LOCALE"
|
|
}
|
|
|
|
screen_summary() {
|
|
clear
|
|
header "Installation Summary"
|
|
echo ""
|
|
|
|
local enc_status="No"
|
|
[[ "$LUKS" == "true" ]] && enc_status="Yes (uses login password)"
|
|
|
|
gum style --foreground 15 --border normal --border-foreground 8 \
|
|
--padding "0 2" --width 60 \
|
|
"Disk: ${DISK}" \
|
|
"Encryption: ${enc_status}" \
|
|
"Filesystem: ${FILESYSTEM}" \
|
|
"Username: ${USERNAME}" \
|
|
"Hostname: ${HOSTNAME}" \
|
|
"Keymap: ${KEYMAP}" \
|
|
"Locale: ${LOCALE}" \
|
|
"Timezone: ${TIMEZONE}" \
|
|
"Git: ${GIT_NAME} <${GIT_EMAIL}>"
|
|
|
|
echo ""
|
|
gum confirm --affirmative "Install" --negative "Abort" \
|
|
"Proceed with installation?" || die "Installation aborted by user."
|
|
}
|
|
|
|
# ── Install steps ─────────────────────────────────────────────────────────────
|
|
|
|
install_partition() {
|
|
info "Partitioning ${DISK}..."
|
|
log "Partitioning $DISK"
|
|
|
|
wipefs -af "$DISK" 2>>"$LOG"
|
|
sgdisk --zap-all "$DISK" 2>>"$LOG"
|
|
|
|
parted -s "$DISK" \
|
|
mklabel gpt \
|
|
mkpart ESP fat32 1MiB "${ESP_SIZE}" \
|
|
set 1 esp on \
|
|
mkpart root "${ESP_SIZE}" 100%
|
|
|
|
partprobe "$DISK"
|
|
udevadm settle --timeout=30
|
|
|
|
wipefs -af "$(part 1)" "$(part 2)"
|
|
|
|
ok "Partitioned."
|
|
}
|
|
|
|
install_luks() {
|
|
[[ "$LUKS" != "true" ]] && return
|
|
|
|
info "Setting up LUKS encryption on $(part 2)..."
|
|
log "LUKS format $(part 2)"
|
|
|
|
printf '%s' "$PASS" | cryptsetup luksFormat \
|
|
--batch-mode --type luks2 --key-file=- "$(part 2)"
|
|
printf '%s' "$PASS" | cryptsetup open \
|
|
--key-file=- "$(part 2)" cryptroot
|
|
|
|
ok "LUKS unlocked."
|
|
}
|
|
|
|
install_format_mount() {
|
|
info "Formatting..."
|
|
mkfs.fat -F32 -n ESP "$(part 1)" 2>>"$LOG"
|
|
|
|
local root_dev
|
|
if [[ "$LUKS" == "true" ]]; then
|
|
root_dev="/dev/mapper/cryptroot"
|
|
else
|
|
root_dev="$(part 2)"
|
|
fi
|
|
|
|
if [[ "$FILESYSTEM" == "btrfs" ]]; then
|
|
mkfs.btrfs -f -L root "$root_dev" 2>>"$LOG"
|
|
else
|
|
mkfs.ext4 -F -L root "$root_dev" 2>>"$LOG"
|
|
fi
|
|
|
|
info "Mounting..."
|
|
mount "$root_dev" "$TARGET"
|
|
mkdir -p "${TARGET}/boot"
|
|
mount "$(part 1)" "${TARGET}/boot"
|
|
|
|
ok "Formatted and mounted."
|
|
}
|
|
|
|
install_pacstrap() {
|
|
info "Building package list..."
|
|
|
|
local pkglist="/run/archiso/bootmnt/arch/pkglist.x86_64.txt"
|
|
local pkgs=()
|
|
local dropped=()
|
|
local filter_ok=false
|
|
|
|
if [[ -f "$pkglist" ]]; then
|
|
# The medium's pkglist comes from `pacman -Q`: lines are "name version".
|
|
# Parse only the first field, then drop names not resolvable in the live
|
|
# session's repos (e.g. local-aur packages are not configured there).
|
|
info "Syncing package databases for resolvability check..."
|
|
if pacman -Syy --noconfirm >>"$LOG" 2>&1; then
|
|
declare -A available=()
|
|
local db_line
|
|
while IFS= read -r db_line; do
|
|
[[ -n "$db_line" ]] && available["$db_line"]=1
|
|
done < <(pacman -Slq 2>>"$LOG" || true)
|
|
(( ${#available[@]} > 0 )) && filter_ok=true
|
|
else
|
|
warn "pacman -Syy failed (see $LOG) — installing without resolvability filter."
|
|
fi
|
|
|
|
local line pkg
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
pkg=${line%% *}
|
|
skip "$pkg" && continue
|
|
if $filter_ok && [[ -z "${available[$pkg]:-}" ]]; then
|
|
dropped+=("$pkg")
|
|
continue
|
|
fi
|
|
pkgs+=("$pkg")
|
|
done < "$pkglist"
|
|
fi
|
|
|
|
if (( ${#dropped[@]} > 0 )); then
|
|
warn "Skipped ${#dropped[@]} pkglist package(s) not found in configured repos:"
|
|
gum style --foreground 11 "${dropped[@]/#/ - }"
|
|
local d
|
|
for d in "${dropped[@]}"; do
|
|
log "Pacstrap: skipped unresolvable: $d"
|
|
done
|
|
fi
|
|
|
|
# Hardcoded essentials are never filtered — they must be in allowed repos.
|
|
for p in base linux linux-firmware linux-cachyos linux-cachyos-headers \
|
|
networkmanager grub efibootmgr sudo git intel-ucode amd-ucode \
|
|
mkinitcpio cryptsetup dosfstools e2fsprogs btrfs-progs \
|
|
cachyos-keyring cachyos-mirrorlist; do
|
|
local found=false
|
|
for existing in "${pkgs[@]+"${pkgs[@]}"}"; do
|
|
[[ "$existing" == "$p" ]] && found=true && break
|
|
done
|
|
$found || pkgs+=("$p")
|
|
done
|
|
|
|
info "Installing ${#pkgs[@]} packages (this may take a while)..."
|
|
log "Pacstrap: ${#pkgs[@]} packages, ${#dropped[@]} skipped"
|
|
pacstrap "$TARGET" "${pkgs[@]}" --noconfirm 2>>"$LOG"
|
|
|
|
ok "Packages installed."
|
|
}
|
|
|
|
install_fstab() {
|
|
info "Generating fstab..."
|
|
genfstab -U "$TARGET" >> "${TARGET}/etc/fstab"
|
|
ok "fstab generated."
|
|
}
|
|
|
|
install_cachyos_repo() {
|
|
info "Configuring CachyOS repository..."
|
|
|
|
if ! grep -q '\[cachyos\]' "${TARGET}/etc/pacman.conf" 2>/dev/null; then
|
|
cat >> "${TARGET}/etc/pacman.conf" <<'REPO'
|
|
|
|
[cachyos]
|
|
Include = /etc/pacman.d/cachyos-mirrorlist
|
|
REPO
|
|
fi
|
|
|
|
# [local-aur] lives only on the live medium (file:///opt/localrepo). The
|
|
# target's pacman.conf does not normally contain it - pacstrap is run
|
|
# without -P, so it keeps the stock file shipped by the pacman package -
|
|
# but drop it anyway in case that ever changes.
|
|
sed -i '/^\[local-aur\]$/,/^$/d' "${TARGET}/etc/pacman.conf" 2>/dev/null || true
|
|
|
|
ok "CachyOS repo configured."
|
|
}
|
|
|
|
install_timezone() {
|
|
info "Setting timezone..."
|
|
arch-chroot "$TARGET" ln -sf "/usr/share/zoneinfo/${TIMEZONE}" /etc/localtime
|
|
arch-chroot "$TARGET" hwclock --systohc || true
|
|
ok "Timezone set."
|
|
}
|
|
|
|
install_locale() {
|
|
info "Setting locale..."
|
|
|
|
arch-chroot "$TARGET" sed -i "s/^#${LOCALE}/${LOCALE}/" /etc/locale.gen
|
|
arch-chroot "$TARGET" locale-gen
|
|
|
|
echo "LANG=${LOCALE}" > "${TARGET}/etc/locale.conf"
|
|
echo "KEYMAP=${KEYMAP}" > "${TARGET}/etc/vconsole.conf"
|
|
|
|
ok "Locale and keymap set."
|
|
}
|
|
|
|
install_hostname() {
|
|
info "Setting hostname..."
|
|
echo "$HOSTNAME" > "${TARGET}/etc/hostname"
|
|
cat > "${TARGET}/etc/hosts" <<EOF
|
|
127.0.0.1 localhost
|
|
::1 localhost
|
|
127.0.1.1 ${HOSTNAME}.localdomain ${HOSTNAME}
|
|
EOF
|
|
ok "Hostname set."
|
|
}
|
|
|
|
install_network() {
|
|
info "Configuring network..."
|
|
|
|
arch-chroot "$TARGET" systemctl enable NetworkManager
|
|
|
|
local nm_dir="/etc/NetworkManager/system-connections"
|
|
if [[ -d "$nm_dir" ]] && ls "$nm_dir"/* &>/dev/null; then
|
|
mkdir -p "${TARGET}/etc/NetworkManager/system-connections"
|
|
cp -a "$nm_dir"/* "${TARGET}/etc/NetworkManager/system-connections/"
|
|
chmod 600 "${TARGET}/etc/NetworkManager/system-connections"/*
|
|
log "WiFi profiles copied."
|
|
fi
|
|
|
|
ok "NetworkManager enabled."
|
|
}
|
|
|
|
install_user() {
|
|
info "Creating user..."
|
|
|
|
cp -rT /etc/skel "${TARGET}/etc/skel"
|
|
|
|
arch-chroot "$TARGET" useradd -m -G wheel -s /bin/bash "$USERNAME"
|
|
|
|
printf '%s:%s\n' "$USERNAME" "$PASS" | arch-chroot "$TARGET" chpasswd
|
|
|
|
arch-chroot "$TARGET" passwd -l root
|
|
|
|
echo '%wheel ALL=(ALL:ALL) ALL' > "${TARGET}/etc/sudoers.d/10-wheel"
|
|
chmod 440 "${TARGET}/etc/sudoers.d/10-wheel"
|
|
|
|
ok "User ${USERNAME} created (wheel group, sudo enabled). Root locked."
|
|
}
|
|
|
|
install_git() {
|
|
info "Configuring git..."
|
|
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
|
git config --global user.name "$GIT_NAME"
|
|
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
|
git config --global user.email "$GIT_EMAIL"
|
|
ok "Git configured for ${USERNAME}."
|
|
}
|
|
|
|
install_end4_dots() {
|
|
if [[ ! -d /opt/dots-hyprland ]]; then
|
|
warn "No staged dots-hyprland at /opt/dots-hyprland — skipping end-4 dotfiles."
|
|
return
|
|
fi
|
|
|
|
info "Installing end-4 dotfiles (this may take a while)..."
|
|
|
|
local home_in_target="/home/${USERNAME}"
|
|
local repo_in_target="${home_in_target}/.dots-hyprland"
|
|
local repo_on_host="${TARGET}${repo_in_target}"
|
|
local dots_ok=true
|
|
local setups_done=true
|
|
|
|
# These chowns run on the live medium, whose /etc/passwd has no
|
|
# $USERNAME — the user exists only inside $TARGET — so name lookup fails
|
|
# ("chown: invalid user"). Resolve the numeric ids from the target and
|
|
# chown by number; the catch-all at the end of this function does the same.
|
|
local owner
|
|
owner="$(arch-chroot "$TARGET" id -u "$USERNAME"):$(arch-chroot "$TARGET" id -g "$USERNAME")"
|
|
|
|
cp -a /opt/dots-hyprland "$repo_on_host"
|
|
chown -R "$owner" "$repo_on_host"
|
|
|
|
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "${TARGET}/etc/sudoers.d/99-end4-install"
|
|
chmod 440 "${TARGET}/etc/sudoers.d/99-end4-install"
|
|
|
|
# Everything appended to $LOG from here belongs to this attempt; used
|
|
# further down to report which steps end-4 had to skip.
|
|
local log_mark=0
|
|
if [[ -f "$LOG" ]]; then
|
|
log_mark=$(wc -l < "$LOG")
|
|
fi
|
|
|
|
# stdin feeds "i" to end-4's x() [R/e:i] failure prompt, which otherwise
|
|
# blocks forever on a TTY whose prompt text is redirected to $LOG.
|
|
# Its systemd lines cannot work from inside this chroot: the medium's /run
|
|
# is bind-mounted in, so `systemctl --machine=…@.host --user enable … --now`
|
|
# has no target session (and --now would start services on the live medium
|
|
# anyway) - they fail on every install. "i" lets the run carry on with the
|
|
# venv, groups and config sync instead of aborting at the first such line;
|
|
# the usable part of that step is re-applied below and every skip is
|
|
# reported afterwards.
|
|
# --skip-alldeps: every dependency was already installed at ISO build time
|
|
# (customize_airootfs.sh runs ./setup install-deps), so this only copies
|
|
# configs, sets up services and builds the python venv from the warm cache.
|
|
if ! arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
|
env HOME="$home_in_target" bash -c \
|
|
"cd ${repo_in_target} && ./setup install -f -s --skip-alldeps" \
|
|
< <(yes i) >>"$LOG" 2>&1; then
|
|
warn "end-4 setup failed, retrying files only (see $LOG)."
|
|
setups_done=false
|
|
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
|
env HOME="$home_in_target" bash -c \
|
|
"cd ${repo_in_target} && ./setup install -f -s --skip-alldeps --skip-allsetups" \
|
|
< <(yes i) >>"$LOG" 2>&1 || dots_ok=false
|
|
fi
|
|
|
|
rm -f "${TARGET}/etc/sudoers.d/99-end4-install"
|
|
|
|
if ! arch-chroot "$TARGET" id -nG "$USERNAME" | grep -qw input; then
|
|
arch-chroot "$TARGET" groupadd -f i2c
|
|
arch-chroot "$TARGET" usermod -aG video,i2c,input "$USERNAME"
|
|
warn "end-4 group setup was missing and has been applied."
|
|
fi
|
|
|
|
# ── steps end-4 cannot take from inside the chroot ───────────────────────
|
|
# `enable` alone is a pure file operation and needs no running session, so
|
|
# redo those here - without --now, since the target is not booted yet.
|
|
local mods_conf="${TARGET}/etc/modules-load.d/i2c-dev.conf"
|
|
if [[ ! -f "$mods_conf" ]]; then
|
|
mkdir -p "$(dirname "$mods_conf")"
|
|
echo i2c-dev > "$mods_conf"
|
|
fi
|
|
|
|
if [[ -f "${TARGET}/usr/lib/systemd/system/bluetooth.service" ]]; then
|
|
arch-chroot "$TARGET" systemctl enable bluetooth.service >>"$LOG" 2>&1 \
|
|
|| warn "Could not enable bluetooth.service (see $LOG)."
|
|
fi
|
|
# `systemctl --user` cannot work in this chroot either: it needs
|
|
# /run/user/UID/bus, which does not exist until the target boots (it only
|
|
# ever reports "Failed to connect to user scope bus"). Write the symlink
|
|
# `enable` would write — the same absolute link systemctl produces.
|
|
if [[ -f "${TARGET}/usr/lib/systemd/user/ydotool.service" ]]; then
|
|
local yd_wants="${TARGET}${home_in_target}/.config/systemd/user/default.target.wants"
|
|
if mkdir -p "$yd_wants" \
|
|
&& ln -sfn /usr/lib/systemd/user/ydotool.service "$yd_wants/ydotool.service"; then
|
|
log "ydotool.service enabled for ${USERNAME} (symlinked in user unit dir)."
|
|
else
|
|
warn "Could not enable ydotool.service for ${USERNAME} (see $LOG)."
|
|
fi
|
|
fi
|
|
|
|
# Everything end-4 answered "i" to. systemctl/gsettings are the lines known
|
|
# to be unusable in this chroot; anything else it had to skip is worth
|
|
# telling the user about.
|
|
local -a ignored=()
|
|
if ((log_mark > 0)); then
|
|
mapfile -t ignored < <(
|
|
sed -n "$((log_mark + 1)),\$p" "$LOG" | sed $'s/\x1b\\[[0-9;]*m//g' |
|
|
sed -n 's/^.*Command "\(.*\)" has failed but ignored by user\..*$/\1/p'
|
|
)
|
|
fi
|
|
if ((${#ignored[@]} > 0)); then
|
|
log "end-4 skipped ${#ignored[@]} step(s): ${ignored[*]}"
|
|
local step
|
|
local -a skipped=()
|
|
for step in "${ignored[@]}"; do
|
|
case "$step" in
|
|
*systemctl* | *gsettings*) ;;
|
|
*) skipped+=("$step") ;;
|
|
esac
|
|
done
|
|
if ((${#skipped[@]} > 0)); then
|
|
warn "end-4 had to skip: ${skipped[*]} (see $LOG)"
|
|
fi
|
|
fi
|
|
if ! $setups_done; then
|
|
warn "end-4 setup did not complete — rerun as ${USERNAME}: cd ~/.dots-hyprland && ./setup install-setups"
|
|
fi
|
|
|
|
# end-4's install step rsync --delete's the whole ~/.config/quickshell
|
|
# directory (sdata/subcmd-install/3.files-legacy.sh), which drops the
|
|
# end4-pC fork skel put into the user's home - restore it afterwards.
|
|
local fork_src="/etc/skel/.config/quickshell/end4-pC"
|
|
local fork_dst="${TARGET}${home_in_target}/.config/quickshell/end4-pC"
|
|
if [[ -d "$fork_src" ]]; then
|
|
mkdir -p "$(dirname "$fork_dst")"
|
|
rm -rf "$fork_dst"
|
|
cp -a "$fork_src" "$fork_dst"
|
|
fi
|
|
|
|
# Same for fish: end-4 syncs ~/.config/fish with rsync --delete, keeping
|
|
# only what upstream ships (auto-Hypr.fish, config.fish, fish_variables),
|
|
# which drops skel's extras (fisher/nvm functions, completions,
|
|
# fish_plugins) and restores its own `alias q 'qs -c ii'`. The default
|
|
# quickshell config on this ISO is end4-pC, so put the extras back and
|
|
# repoint that one alias.
|
|
local fish_src="/etc/skel/.config/fish"
|
|
local fish_dst="${TARGET}${home_in_target}/.config/fish"
|
|
if [[ -d "$fish_src" && -d "$fish_dst" ]]; then
|
|
rsync -a --exclude config.fish --exclude fish_variables \
|
|
"$fish_src/" "$fish_dst/"
|
|
if [[ -f "${fish_dst}/config.fish" ]]; then
|
|
sed -i "s/alias q 'qs -c ii'/alias q 'qs -c end4-pC'/" "${fish_dst}/config.fish"
|
|
grep -qF "alias q 'qs -c end4-pC'" "${fish_dst}/config.fish" \
|
|
|| warn "fish 'q' alias still points at the upstream ii config (see $LOG)."
|
|
fi
|
|
fi
|
|
|
|
local configs_ok=true
|
|
[[ -e "${TARGET}${home_in_target}/.config/hypr/hyprland.lua" \
|
|
&& -d "${TARGET}${home_in_target}/.config/quickshell/ii" \
|
|
&& -d "${TARGET}${home_in_target}/.config/quickshell/end4-pC" ]] || configs_ok=false
|
|
# Files alone are not enough: with --skip-alldeps the dependencies must
|
|
# have come from the ISO. hyprland and quickshell are installed at ISO
|
|
# build time, so their binaries missing here means pacstrap dropped them
|
|
# (they were not resolvable in the live repos). The venv is created from
|
|
# the image's uv cache at install time, so a missing python means the
|
|
# python step of end-4's setup never got to run.
|
|
if $configs_ok; then
|
|
[[ -x "${TARGET}/usr/bin/hyprctl" && -x "${TARGET}/usr/bin/quickshell" ]] \
|
|
|| configs_ok=false
|
|
[[ -x "${TARGET}${home_in_target}/.local/state/quickshell/.venv/bin/python" ]] \
|
|
|| configs_ok=false
|
|
fi
|
|
|
|
if [[ -d /etc/skel/.config/hypr/custom ]]; then
|
|
cp -rfT /etc/skel/.config/hypr/custom "${TARGET}${home_in_target}/.config/hypr/custom"
|
|
fi
|
|
|
|
# Presets ship with absolute paths from the build machine; repoint them at
|
|
# the home that was just created (the images they name are not shipped).
|
|
local presets="${TARGET}${home_in_target}/.config/illogical-impulse/presets"
|
|
local preset
|
|
for preset in "$presets"/*.json; do
|
|
[[ -f "$preset" ]] || continue
|
|
sed -i "s|/home/alvin/|/home/${USERNAME}/|g" "$preset"
|
|
done
|
|
|
|
# Everything above that ran as root from the live medium (repo copy,
|
|
# fork/fish/hypr restores, and the presets sed -i just rewrote through a
|
|
# temp file) left root-owned files in the home — one pass fixes them all.
|
|
# Numeric ids again: this chown also runs on the medium, not in the chroot.
|
|
chown -R "$owner" "${TARGET}${home_in_target}"
|
|
|
|
if $dots_ok && $configs_ok; then
|
|
log "end-4 dotfiles installed for ${USERNAME} (setups_done=${setups_done} skipped=${#ignored[@]})."
|
|
ok "end-4 dotfiles installed."
|
|
else
|
|
log "end-4 dotfiles incomplete for ${USERNAME} (setup_exit_ok=${dots_ok} configs=${configs_ok} setups=${setups_done} skipped=${#ignored[@]})."
|
|
warn "end-4 dotfiles incomplete (see $LOG) — as ${USERNAME}, run: cd ~/.dots-hyprland && ./setup install"
|
|
fi
|
|
}
|
|
|
|
install_mkinitcpio() {
|
|
info "Generating initramfs..."
|
|
|
|
if [[ "$LUKS" == "true" ]]; then
|
|
mkdir -p "${TARGET}/etc/mkinitcpio.conf.d"
|
|
cat > "${TARGET}/etc/mkinitcpio.conf.d/alvin.conf" <<HOOKS
|
|
HOOKS=(base udev autodetect modconf kms keyboard keymap consolefont block encrypt filesystems fsck)
|
|
HOOKS
|
|
fi
|
|
|
|
arch-chroot "$TARGET" mkinitcpio -P
|
|
|
|
ok "initramfs generated."
|
|
}
|
|
|
|
install_bootloader() {
|
|
info "Installing GRUB bootloader..."
|
|
|
|
if [[ "$LUKS" == "true" ]]; then
|
|
local luks_uuid
|
|
luks_uuid=$(blkid -s UUID -o value "$(part 2)")
|
|
sed -i "s|^GRUB_CMDLINE_LINUX=.*|GRUB_CMDLINE_LINUX=\"cryptdevice=UUID=${luks_uuid}:cryptroot\"|" \
|
|
"${TARGET}/etc/default/grub"
|
|
fi
|
|
|
|
arch-chroot "$TARGET" grub-install \
|
|
--target=x86_64-efi --efi-directory=/boot --bootloader-id=ALVIN
|
|
arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg
|
|
|
|
ok "Bootloader installed."
|
|
}
|
|
|
|
install_cleanup() {
|
|
info "Cleaning up..."
|
|
cp "$LOG" "${TARGET}/var/log/alvin-install.log" 2>/dev/null || true
|
|
unset PASS
|
|
log "Installation complete."
|
|
ok "Installation complete!"
|
|
echo ""
|
|
gum style --foreground 10 --align center \
|
|
"Arch Linux installed successfully." \
|
|
"" \
|
|
"Run 'reboot' to boot into your new system."
|
|
echo ""
|
|
}
|
|
|
|
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
|
|
main() {
|
|
check_prereqs
|
|
|
|
screen_welcome
|
|
screen_keymap
|
|
screen_network
|
|
screen_disk
|
|
screen_luks
|
|
screen_filesystem
|
|
screen_username
|
|
screen_password
|
|
screen_hostname
|
|
screen_git
|
|
screen_timezone
|
|
screen_locale
|
|
screen_summary
|
|
|
|
install_partition
|
|
install_luks
|
|
install_format_mount
|
|
install_pacstrap
|
|
install_fstab
|
|
install_cachyos_repo
|
|
install_timezone
|
|
install_locale
|
|
install_hostname
|
|
install_network
|
|
install_user
|
|
install_git
|
|
install_end4_dots
|
|
install_mkinitcpio
|
|
install_bootloader
|
|
install_cleanup
|
|
}
|
|
|
|
main "$@"
|