veilor-os/overlay/usr/local/sbin/veilor-installer
veilor-org fc7c3f858b v0.5.0-beta: fix 4 installer blockers found during lint
Bugs found by agent linter on v0.5.0-alpha:

1. logvol missing --size: ksvalidator rejected. Added --size=8192 --grow.
2. bootloader --location=mbr on UEFI: conflicts with /boot/efi part.
   Switched to --location=none (anaconda auto-detects EFI vs BIOS).
3. lsblk awk truncated multi-word disk models ("WD PC SN740" → "WD").
   Now collapses model spaces to underscores, preserves full string.
   Also added mmcblk to disk regex (eMMC support).
4. Heredoc with $VAR expansion + passwords containing $/`/" corrupted
   generated ks. Now: single-quoted heredoc + sed placeholder
   substitution. Plus input validator rejects "$\` chars in passwords.

ksvalidator clean on sample generated ks.
bash -n clean.

CI build still in flight (3328ffb). This pushes a new commit; CI will
run again with these fixes. Net delay: zero (3328ffb's installer was
broken anyway, so its ISO unusable for install path).
2026-05-02 03:42:15 +01:00

297 lines
9 KiB
Bash

#!/usr/bin/env bash
# veilor-os installer — TUI wrapper around anaconda kickstart install.
# Runs on tty1 in place of getty (live ISO boot path).
#
# Flow:
# 1. ASCII banner
# 2. Menu: Install / Live shell / Reboot / Power off
# 3. If Install: collect answers via whiptail (disk, hostname, LUKS pw,
# admin pw, locale)
# 4. Generate /run/install/veilor-generated.ks from template + answers
# 5. Exec anaconda --kickstart=/run/install/veilor-generated.ks
# 6. On finish: reboot into installed system
#
# v0.5.0 — first cut. v0.5.1 swaps whiptail for gum (Go TUI, prettier).
set -uo pipefail
export TERM="${TERM:-linux}"
LOG=/var/log/veilor-installer.log
exec > >(tee -a "$LOG") 2>&1
banner() {
clear
cat << 'EOF'
▌ ▌▙▀▖▌ ▐▌▛▀▖▌ ▖▙▀▖▖▖
▙▖▌█ ▐▖▟▘▙▄▘▌ ▌▌ ▙▟
veilor-os installer
hardened. branded. yours.
EOF
echo "──────────────────────────────────────────"
echo
}
require_tty() {
if ! [[ -t 0 && -t 1 ]]; then
echo "[ERR] veilor-installer must run on a real tty" >&2
exit 1
fi
}
main_menu() {
local choice
choice=$(whiptail --title "veilor-os" \
--menu "Welcome. What would you like to do?" 16 60 5 \
"1" "Install veilor-os to disk" \
"2" "Try live — desktop (KDE Plasma)" \
"3" "Try live — shell" \
"4" "Reboot" \
"5" "Power off" \
3>&1 1>&2 2>&3)
echo "$choice"
}
collect_answers() {
local disk hostname luks_pw admin_pw locale
local disks_list
# ── Disk ──
# Build "tag description" pairs for whiptail. Model strings have spaces
# (e.g. "WD PC SN740"), so collapse model to underscores for menu.
disks_list=$(lsblk -dpno NAME,SIZE,MODEL | grep -E '^/dev/(sd|nvme|vd|mmcblk)' | \
awk '{name=$1; size=$2; $1=""; $2=""; sub(/^ +/,""); gsub(/ /,"_"); model=$0; if(model=="")model="unknown"; print name, size"_"model}')
if [[ -z $disks_list ]]; then
whiptail --title "veilor-os" --msgbox "No installable disks found." 8 50
return 1
fi
disk=$(whiptail --title "Select install disk" \
--menu "WARNING: selected disk will be ERASED." 18 70 8 \
$disks_list 3>&1 1>&2 2>&3) || return 1
# ── Hostname ──
hostname=$(whiptail --title "Hostname" \
--inputbox "Set hostname:" 10 60 "veilor" \
3>&1 1>&2 2>&3) || return 1
# Reject shell-special chars in passwords. Generated kickstart writes
# them via heredoc + sed substitution; bare $, ", \, ` would corrupt
# the ks line or partially expand. 8-char min for entropy.
validate_pw() {
local pw=$1 label=$2
if [[ ${#pw} -lt 8 ]]; then
whiptail --title "Weak $label" --msgbox "Min 8 chars." 8 40
return 1
fi
if [[ $pw =~ [\"\$\\\`] ]]; then
whiptail --title "Invalid $label" --msgbox \
"Cannot contain: \" \$ \\ \`" 8 50
return 1
fi
return 0
}
# ── LUKS passphrase ──
luks_pw=$(whiptail --title "Disk encryption" \
--passwordbox "LUKS passphrase (full-disk encryption):" 10 60 \
3>&1 1>&2 2>&3) || return 1
validate_pw "$luks_pw" "passphrase" || return 1
# ── Admin password ──
admin_pw=$(whiptail --title "Admin password" \
--passwordbox "Admin user password (login after install):" 10 60 \
3>&1 1>&2 2>&3) || return 1
validate_pw "$admin_pw" "password" || return 1
# ── Locale ──
locale=$(whiptail --title "Locale" \
--menu "Choose locale:" 14 50 4 \
"en_GB.UTF-8" "English (UK)" \
"en_US.UTF-8" "English (US)" \
"de_DE.UTF-8" "Deutsch" \
"fr_FR.UTF-8" "Francais" \
3>&1 1>&2 2>&3) || return 1
# ── Confirmation ──
whiptail --title "Confirm install" --yesno \
"About to install veilor-os:
Disk: $disk (will be ERASED)
Hostname: $hostname
Locale: $locale
LUKS: set
Admin pw: set
Proceed?" 16 60 || return 1
# Export to caller via globals
SEL_DISK=$disk
SEL_HOSTNAME=$hostname
SEL_LUKS_PW=$luks_pw
SEL_ADMIN_PW=$admin_pw
SEL_LOCALE=$locale
return 0
}
generate_ks() {
# Build kickstart for actual disk install.
# NOTE: passwords go in via --plaintext to avoid storing crypted hash
# collisions; anaconda hashes per /etc/login.defs at install time.
local out=/run/install/veilor-generated.ks
local disk_basename
disk_basename=$(basename "$SEL_DISK")
mkdir -p /run/install
# Single-quoted heredoc → no shell expansion. Substitute placeholders
# via sed afterwards. Bulletproof against $/`/" in passwords.
cat > "$out" << 'KSEOF' || return 1
# veilor-os installer-generated kickstart
# DO NOT commit this file — secrets inline.
url --mirrorlist="https://mirrors.fedoraproject.org/mirrorlist?repo=fedora-43&arch=x86_64"
repo --name=fedora --baseurl="https://download.fedoraproject.org/pub/fedora/linux/releases/43/Everything/x86_64/os/" --install
repo --name=updates --baseurl="https://download.fedoraproject.org/pub/fedora/linux/updates/43/Everything/x86_64/" --install
keyboard --xlayouts='us'
lang __LOCALE__
timezone Europe/London --utc
firstboot --disable
eula --agreed
selinux --enforcing
services --enabled=sshd,fail2ban,usbguard,tuned,auditd,firewalld,chronyd,sddm
network --bootproto=dhcp --device=link --activate --hostname=__HOSTNAME__
firewall --enabled --service=ssh
rootpw --lock
user --name=admin --groups=wheel --gecos="veilor admin" --password=__ADMIN_PW__ --plaintext
# Full hardening cmdline (installed system, not live):
# --location=none: anaconda auto-places bootloader (UEFI grub2-efi or BIOS).
bootloader --location=none --append="lockdown=integrity slab_nomerge init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on vsyscall=none"
# Disk: zero, LUKS2 (argon2id), btrfs subvolumes
zerombr
clearpart --all --initlabel --drives=__DISK_BASENAME__
part /boot/efi --fstype=efi --size=600
part /boot --fstype=ext4 --size=1024
part pv.veilor --grow --encrypted --luks-version=luks2 --pbkdf=argon2id --passphrase=__LUKS_PW__
volgroup veilor pv.veilor
logvol / --vgname=veilor --name=root --fstype=btrfs --size=8192 --grow
%packages --excludedocs
@^kde-desktop-environment
@kde-apps
@core
@hardware-support
@standard
fail2ban
fail2ban-firewalld
usbguard
usbguard-tools
audit
policycoreutils-python-utils
tuned
chrony
firewalld
plymouth
git
vim-enhanced
tmux
htop
podman
NetworkManager
NetworkManager-wifi
fontconfig
fira-code-fonts
zram-generator
-abrt*
-snapd
-kde-connect
-mlocate
%end
# Reboot when done
reboot
KSEOF
# Substitute placeholders. Use | as sed delimiter (passwords might
# contain /). Forbidden chars in passwords (validated upstream): "$\`
# — sed safe.
sed -i \
-e "s|__LOCALE__|$SEL_LOCALE|" \
-e "s|__HOSTNAME__|$SEL_HOSTNAME|" \
-e "s|__DISK_BASENAME__|$disk_basename|" \
-e "s|__LUKS_PW__|$SEL_LUKS_PW|" \
-e "s|__ADMIN_PW__|$SEL_ADMIN_PW|" \
"$out"
echo "[INFO] generated kickstart at $out"
return 0
}
run_install() {
whiptail --title "Installing" --infobox \
"Installing veilor-os to $SEL_DISK ...
This will take 10-30 minutes.
Logs: /var/log/veilor-installer.log + /tmp/anaconda.log" 10 60
sleep 2
# Hand off to anaconda. --kickstart runs unattended.
if anaconda --kickstart=/run/install/veilor-generated.ks; then
whiptail --title "Done" --msgbox \
"Install complete. System will reboot.
Remove the install media after shutdown." 10 50
sleep 3
systemctl reboot
else
whiptail --title "Install failed" --msgbox \
"Anaconda exited non-zero.
Logs at /tmp/anaconda.log + /var/log/veilor-installer.log.
Press OK to drop to shell." 12 60
return 1
fi
}
drop_to_shell() {
clear
cat << 'EOF'
═══════════════════════════════════════════════════
veilor-os live shell
═══════════════════════════════════════════════════
You are in a live, in-memory environment.
Nothing persists across reboot.
Re-run the installer: sudo veilor-installer
Reboot: sudo systemctl reboot
Power off: sudo systemctl poweroff
EOF
exec /bin/bash --login
}
# ── Entry ──
require_tty
banner
launch_desktop() {
clear
echo "Launching KDE Plasma..."
sleep 1
systemctl isolate graphical.target
# systemd-isolate switches target; sddm spawns on tty1.
# If user logs out, they come back here. Loop continues.
}
while true; do
case "$(main_menu)" in
1)
if collect_answers && generate_ks; then
run_install || continue
fi
;;
2) launch_desktop ;;
3) drop_to_shell ;;
4) systemctl reboot ;;
5) systemctl poweroff ;;
*) drop_to_shell ;;
esac
done