#!/bin/sh
# arctic-strap - install Arctic Linux from a declarative config file
#
#   arctic-strap                   edit /etc/arctic/install.conf, then just this
#   arctic-strap -c file           install from a specific config file instead
#   arctic-strap /mnt              install onto an already-mounted target
#   arctic-strap -f                skip the "already installed here" prompt
#
# There are no menus and no questions - edit the config, then run this:
#
#   vi /etc/arctic/install.conf
#   arctic-strap
#
# Setting A_DISK in the config (e.g. A_DISK=/dev/sda) partitions, formats,
# mounts and installs the bootloader automatically - the whole machine, one
# command, nothing to pre-mount by hand. Leaving A_DISK unset is the manual
# path for anyone who wants to partition themselves: mount a target and pass
# it on the command line, same as always.
#
#   mount /dev/nvme0n1p1 /mnt
#   arctic-strap /mnt
#
# With no config file at all it installs with the built-in defaults below (a
# console-only system, no user account, root locked) rather than refusing,
# the same way pacstrap does not ask you anything either.
#
# shellcheck shell=sh disable=SC2039,SC2059,SC2086

set -u

LIB=/usr/lib/arctic
for d in "$LIB" "$(dirname "$0")/lib" ./lib; do
	[ -f "$d/tui.sh" ] && { . "$d/tui.sh"; TUILIB=$d; break; }
done
[ -n "${c_teal:-}" ] || { echo "arctic-strap: cannot find tui.sh" >&2; exit 1; }

for d in /usr/lib/alpm "$(dirname "$0")/../alpm" ./alpm; do
	[ -f "$d/libalpm.sh" ] && { . "$d/libalpm.sh"; break; }
done

VERSION="1.0.0"
CONF=/etc/arctic/install.conf
LOGF=/tmp/arctic-strap.log

# ------------------------------------------------------------------- defaults
# Used as-is for anything the config file leaves unset - either because there
# is no config file at all, or because it only bothered to uncomment a few
# lines.
TARGET=""
A_DISK=""
A_ROOTDEV=""
A_ESP=""
A_BOOT_FW=""
A_BOOTLOADER=""
A_KERNEL="Arctic-base-kernel"
A_DESKTOP="none"
A_DM="none"
A_HOSTNAME="arctic"
A_USER=""
A_USERPASS=""
A_ROOTPASS=""
A_SHELL="/bin/zsh"
A_LOCALE="en_US.UTF-8"
A_LANG="en_US.UTF-8"
A_KEYMAP="us"
A_TZ="UTC"
A_NTP="yes"
A_NET="offline"
A_WIFI_SSID=""
A_WIFI_PSK=""
A_WIFI_IFACE=""
A_FS="ext4"
A_SNAPSHOT="none"
A_BTRFS_OPTS=""
A_ENCRYPT="no"
A_ENCRYPT_PASS=""
A_SWAP="none"
A_SWAPSIZE="4G"
A_SWAPDEV=""
A_GPU="auto"
A_AUDIO="no"
A_BLUETOOTH="no"
A_SSH="no"
A_FIREWALL="yes"
A_TRIM="auto"
A_EXTRA=""
A_FONTS=""
A_PKGS=""

bail() {
	tui_done 2>/dev/null
	{
		printf 'error: %s\n' "$*"
		cat <<'EOF'
   \
    \
        .--.
       |o_o |
       |:_/ |
      //   \ \
     (|     | )
    /'\_   _/`\
    \___)=(___/
EOF
	} >&2
	exit 1
}
logit() { printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*" >>"$LOGF"; }

detect_gpu() {
	if [ -d /sys/bus/pci/devices ]; then
		v=$(lspci 2>/dev/null | grep -iE 'vga|3d|display')
		case "$v" in
		*NVIDIA*|*nVidia*) echo nvidia ;;
		*AMD*|*ATI*|*"Advanced Micro"*) echo amd ;;
		*Intel*) echo intel ;;
		*) echo generic ;;
		esac
	else
		echo generic
	fi
}

detect_microcode() {
	case "$(grep -m1 '^vendor_id' /proc/cpuinfo 2>/dev/null)" in
	*GenuineIntel*) echo intel-ucode ;;
	*AuthenticAMD*) echo amd-ucode ;;
	*) echo none ;;
	esac
}

detect_firmware() { [ -d /sys/firmware/efi ] && echo efi || echo bios; }

# Partition names differ between /dev/sda1 and /dev/nvme0n1p1.
part_name() {
	case "$1" in
	*[0-9]) printf '%sp%s' "$1" "$2" ;;
	*)      printf '%s%s' "$1" "$2" ;;
	esac
}

detect_rotational() {
	# Returns "ssd" when the target device does not spin.
	d=$(df "$TARGET" 2>/dev/null | awk 'NR==2{print $1}' | sed 's|/dev/||; s|p\?[0-9]*$||')
	[ -n "$d" ] || { echo unknown; return; }
	r=$(cat "/sys/block/$d/queue/rotational" 2>/dev/null)
	[ "$r" = "0" ] && echo ssd || echo hdd
}

detect_existing_install() { [ -f "$1/etc/arctic-release" ]; }

# ------------------------------------------------------------- disk partitioning
#
# Only runs when the config sets A_DISK. Leaving it unset keeps the manual
# path: partition and mount a target yourself, same as always.

# cryptsetup is not baked into the live rootfs the way util-linux/e2fsprogs
# are - it comes from the same bundled repo target_alpm installs from later,
# just applied to the live system's own root instead of a chrooted target.
ensure_pkg() {
	have "$2" && return 0
	ALPM_YES=1 alpm ins "$1" >>"$LOGF" 2>&1
	have "$2"
}

list_disks() {
	for d in /sys/block/*; do
		n=$(basename "$d")
		case "$n" in loop*|ram*|sr*|dm-*|md*|zram*) continue ;; esac
		[ -r "$d/size" ] || continue
		sz=$(cat "$d/size" 2>/dev/null)
		[ "${sz:-0}" -gt 0 ] || continue
		printf '%s\n' "$n"
	done
}

# Make the filesystem chosen in the config, and lay out btrfs subvolumes.
make_root_fs() {
	dev=$1
	case "$A_FS" in
	btrfs)
		mkfs.btrfs -f -L arctic-root "$dev" >>"$LOGF" 2>&1 || return 1
		# The @ / @home layout is what both timeshift and snapper expect: the
		# root subvolume can then be rolled back without taking home with it.
		mount "$dev" "$TARGET" >>"$LOGF" 2>&1 || return 1
		btrfs subvolume create "$TARGET/@"          >>"$LOGF" 2>&1 || return 1
		btrfs subvolume create "$TARGET/@home"      >>"$LOGF" 2>&1 || return 1
		btrfs subvolume create "$TARGET/@snapshots" >>"$LOGF" 2>&1 || return 1
		btrfs subvolume create "$TARGET/@log"       >>"$LOGF" 2>&1 || :
		umount "$TARGET" >>"$LOGF" 2>&1 || return 1

		A_BTRFS_OPTS="rw,noatime,compress=zstd:3,space_cache=v2"
		mount -o "$A_BTRFS_OPTS,subvol=@" "$dev" "$TARGET" >>"$LOGF" 2>&1 || return 1
		mkdir -p "$TARGET/home" "$TARGET/.snapshots" "$TARGET/var/log"
		mount -o "$A_BTRFS_OPTS,subvol=@home"      "$dev" "$TARGET/home"      >>"$LOGF" 2>&1 || :
		mount -o "$A_BTRFS_OPTS,subvol=@snapshots" "$dev" "$TARGET/.snapshots" >>"$LOGF" 2>&1 || :
		mount -o "$A_BTRFS_OPTS,subvol=@log"       "$dev" "$TARGET/var/log"   >>"$LOGF" 2>&1 || :
		;;
	xfs)  mkfs.xfs -f -L arctic-root "$dev" >>"$LOGF" 2>&1 || return 1
	      mount "$dev" "$TARGET" >>"$LOGF" 2>&1 || return 1 ;;
	f2fs) mkfs.f2fs -f -l arctic-root "$dev" >>"$LOGF" 2>&1 || return 1
	      mount "$dev" "$TARGET" >>"$LOGF" 2>&1 || return 1 ;;
	ext3) mkfs.ext3 -q -F -L arctic-root "$dev" >>"$LOGF" 2>&1 || return 1
	      mount "$dev" "$TARGET" >>"$LOGF" 2>&1 || return 1 ;;
	ext2) mkfs.ext2 -q -F -L arctic-root "$dev" >>"$LOGF" 2>&1 || return 1
	      mount "$dev" "$TARGET" >>"$LOGF" 2>&1 || return 1 ;;
	zfs)
		# Best-effort: Arctic has no zfs-utils package yet (OpenZFS needs an
		# out-of-tree kernel module), so this path is untested end to end.
		zpool create -f -O mountpoint=none -O compression=zstd \
			-O acltype=posixacl -O xattr=sa -o cachefile=none \
			arctic-root "$dev" >>"$LOGF" 2>&1 || return 1
		zfs create -o mountpoint=legacy arctic-root/root >>"$LOGF" 2>&1 || return 1
		mount -t zfs arctic-root/root "$TARGET" >>"$LOGF" 2>&1 || return 1
		;;
	*)    mkfs.ext4 -q -F -L arctic-root "$dev" >>"$LOGF" 2>&1 || return 1
	      mount "$dev" "$TARGET" >>"$LOGF" 2>&1 || return 1 ;;
	esac
	return 0
}

# Lay down a GPT and format it: one EFI partition when the machine booted
# through UEFI, one root filesystem taking the rest. Sets A_ESP/A_ROOTDEV/
# A_BOOT_FW for do_install's own bootloader step at the end.
partition_disk() {
	disk=$A_DISK
	[ -b "$disk" ] || bail "A_DISK=$disk is not a block device"
	fw=$(detect_firmware)
	A_BOOT_FW=$fw

	step "Partitioning $disk" "wiping old signatures"
	wipefs -a "$disk" >>"$LOGF" 2>&1 || :

	if [ "$fw" = efi ]; then
		step "Partitioning $disk" "GPT: EFI + root"
		# sfdisk reads the whole layout on stdin, which avoids a pile of
		# interactive fdisk keystrokes.
		printf 'label: gpt\n,512M,U\n,,L\n' | sfdisk --quiet "$disk" >>"$LOGF" 2>&1 \
			|| bail "partitioning $disk failed - see $LOGF"
		p1=$(part_name "$disk" 1); p2=$(part_name "$disk" 2)
		step "Formatting $p1" "EFI (FAT32)"
		mkfs.vfat -F32 -n ARCTIC_EFI "$p1" >>"$LOGF" 2>&1 \
			|| bail "mkfs.vfat failed on $p1 - see $LOGF"
	else
		step "Partitioning $disk" "GPT: BIOS boot + root"
		printf 'label: gpt\n,1M,21686148-6449-6E6F-744E-656564454649\n,,L\n' \
			| sfdisk --quiet "$disk" >>"$LOGF" 2>&1 \
			|| bail "partitioning $disk failed - see $LOGF"
		p1=""; p2=$(part_name "$disk" 2)
	fi

	rootsrc=$p2
	if [ "$A_ENCRYPT" = yes ]; then
		step "Encrypting $p2" "LUKS"
		ensure_pkg cryptsetup cryptsetup || bail "cryptsetup could not be installed from the medium"
		# --key-file - reads the whole of stdin as the key verbatim, so this
		# must not add a trailing newline the way echo would - whatever
		# passphrase is typed back at the interactive unlock prompt at boot
		# will not have one either, and the two have to match exactly.
		printf '%s' "$A_ENCRYPT_PASS" | \
			cryptsetup luksFormat -q --key-file - "$p2" >>"$LOGF" 2>&1 \
			|| bail "cryptsetup luksFormat failed on $p2 - see $LOGF"
		printf '%s' "$A_ENCRYPT_PASS" | \
			cryptsetup open --key-file - "$p2" cryptroot >>"$LOGF" 2>&1 \
			|| bail "could not unlock the new encrypted volume - see $LOGF"
		rootsrc=/dev/mapper/cryptroot
	fi

	step "Formatting $rootsrc" "$A_FS"
	mkdir -p "$TARGET"
	make_root_fs "$rootsrc" || bail "could not create the $A_FS filesystem on $rootsrc - see $LOGF"

	if [ -n "$p1" ]; then
		step "Mounting $p1" "$TARGET/boot"
		mkdir -p "$TARGET/boot"
		mount "$p1" "$TARGET/boot" >>"$LOGF" 2>&1 || :
		A_ESP=$p1
	fi
	A_ROOTDEV=$p2
	logit "disk $disk ready and mounted on $TARGET"
}

# ------------------------------------------------------------------ the install

# Package sets, per repo. main is what every Arctic system needs.
PKGS_MAIN="glibc busybox toybox zsh doas libarchive mandoc onetrueawk bmake byacc \
alpm arctic-base arctic-init iana-etc tzdata ca-certificates"
PKGS_NET="iwd dhcpcd openssl curl dbus"
PKGS_EXTRA_BASE="foot vim less htop fastfetch pciutils usbutils"

step() {
	TUI_GAUGE_NOTE="$2"
	tui_gauge "$1"
	logit "$1"
}

run() {
	logit "+ $*"
	if "$@" >>"$LOGF" 2>&1; then
		return 0
	fi
	rc=$?
	logit "! failed ($rc): $*"
	return $rc
}

target_alpm() {
	# alpm honours --root, so the whole install is one package transaction.
	ALPM_ROOT="$TARGET" ALPM_YES=1 alpm "$@"
}

do_install() {
	step "Checking the target" "$TARGET"
	[ -d "$TARGET" ] || bail "$TARGET is not a directory"
	mountpoint -q "$TARGET" 2>/dev/null || \
		logit "warning: $TARGET is not a mount point"
	av=$(df -k "$TARGET" | awk 'NR==2{print $4}')
	[ "${av:-0}" -gt 1048576 ] || bail "$TARGET has less than 1 GiB free"

	step "Creating the directory tree" "filesystem layout"
	for d in boot dev etc home proc root run srv sys tmp var opt \
		usr/bin usr/lib usr/share usr/include usr/local \
		var/log var/cache var/lib var/tmp etc/alpm/repos.d \
		var/lib/alpm/local var/lib/alpm/sync
	do
		mkdir -p "$TARGET/$d"
	done
	chmod 1777 "$TARGET/tmp" "$TARGET/var/tmp"
	chmod 700 "$TARGET/root"
	# Arctic is a merged-/usr system; /bin, /sbin, /lib and /lib64 are
	# symlinks. glibc here installs into usr/lib (slibdir=/usr/lib, no
	# separate lib64), but the host gcc used to build every native package
	# still bakes in /lib64/ld-linux-x86-64.so.2 as the interpreter path -
	# that is the standard x86_64 default regardless of where the library
	# actually lives. /lib64 -> usr/lib64 (a separate, empty directory)
	# looked plausible but left every dynamically linked binary unable to
	# find its own interpreter after boot; it must point at usr/lib, same
	# as the live ISO's own rootfs already does.
	for l in bin sbin lib; do
		[ -e "$TARGET/$l" ] || ln -s "usr/$l" "$TARGET/$l" 2>/dev/null || :
	done
	[ -e "$TARGET/lib64" ] || ln -s usr/lib "$TARGET/lib64" 2>/dev/null || :
	mkdir -p "$TARGET/usr/sbin"

	step "Copying the package database" "repositories"
	for f in /etc/alpm/alpm.conf; do
		[ -f "$f" ] && { mkdir -p "$TARGET/etc/alpm"; cp -f "$f" "$TARGET/etc/alpm/"; }
	done
	cp -f /etc/alpm/repos.d/*.repo "$TARGET/etc/alpm/repos.d/" 2>/dev/null || :
	cp -a /var/lib/alpm/sync/. "$TARGET/var/lib/alpm/sync/" 2>/dev/null || :

	step "Installing the base system" "$PKGS_MAIN"
	if [ "$A_NET" = offline ]; then
		# Everything we need is already on the ISO's own package cache.
		mkdir -p "$TARGET/var/cache/alpm/pkg"
		cp -f /var/cache/alpm/pkg/*.alpmz "$TARGET/var/cache/alpm/pkg/" 2>/dev/null || :
	fi
	target_alpm ins $PKGS_MAIN || bail "the base system failed to install - see $LOGF"

	step "Installing the kernel" "$A_KERNEL"
	# -nomod keeps the kernel out of the bootloader until 'arctic-boot-strap' runs.
	target_alpm ins -nomod "$A_KERNEL" || bail "could not install $A_KERNEL"
	uc=$(detect_microcode)
	[ "$uc" != none ] && target_alpm ins "$uc" || :

	step "Installing firmware" "linux-firmware"
	case "$A_KERNEL" in
	Arctic-libre-kernel) logit "libre kernel: skipping nonfree firmware" ;;
	*) target_alpm ins linux-firmware || warn_soft "firmware not installed" ;;
	esac

	step "Installing network tools" "$PKGS_NET"
	target_alpm ins $PKGS_NET || :

	step "Installing base utilities" "$PKGS_EXTRA_BASE"
	target_alpm ins $PKGS_EXTRA_BASE || :

	step "Installing graphics drivers" "$A_GPU"
	g=$A_GPU
	[ "$g" = auto ] && g=$(detect_gpu)
	case "$g" in
	nvidia)
		# gcc-libs and libglvnd are not optional here: libnvidia-api needs
		# libstdc++.so.6 and libnvidia-rtcore needs libgcc_s.so.1, and glvnd is
		# what lets the NVIDIA GL live alongside mesa. gmake goes in too, so the
		# kernel module can be rebuilt when the kernel changes.
		target_alpm ins gcc-libs libglvnd gmake || :
		target_alpm ins nvidia-drivers nvidia-utils || : ;;
	nouveau) target_alpm ins mesa nouveau-drivers || : ;;
	amd) target_alpm ins mesa vulkan-radeon libva-mesa-driver || : ;;
	intel) target_alpm ins mesa vulkan-intel intel-media-driver || : ;;
	*) target_alpm ins mesa || : ;;
	esac

	step "Installing the desktop" "$A_DESKTOP"
	case "$A_DESKTOP" in
	kde)      target_alpm ins plasma-meta konsole dolphin || : ;;
	hyprland) target_alpm ins hyprland waybar wofi xdg-desktop-portal-hyprland || : ;;
	niri)     target_alpm ins niri waybar fuzzel || : ;;
	dwl)      target_alpm ins dwl somebar || : ;;
	xfce)     target_alpm ins xfce4-meta xorg-server || : ;;
	dwm)      target_alpm ins dwm st dmenu xorg-server || : ;;
	dms)      target_alpm ins dms || : ;;
	none)     logit "no desktop requested" ;;
	esac
	case "${A_DM:-none}" in
	sddm)   target_alpm ins sddm sddm-arctic-theme || : ;;
	xdm)    target_alpm ins xdm || : ;;
	greetd) target_alpm ins greetd tuigreet || : ;;
	esac

	step "Installing extra packages" "${A_EXTRA:-none}"
	[ -n "$A_EXTRA" ] && target_alpm ins $A_EXTRA || :
	step "Installing fonts" "${A_FONTS:-none}"
	[ -n "$A_FONTS" ] && target_alpm ins $A_FONTS || :
	step "Installing selected packages" "${A_PKGS:-none}"
	[ -n "$A_PKGS" ] && target_alpm ins $A_PKGS || :
	[ "$A_AUDIO" = yes ] && target_alpm ins pipewire wireplumber pipewire-alsa || :
	[ "$A_BLUETOOTH" = yes ] && target_alpm ins bluez bluez-utils || :
	[ "$A_SSH" = yes ] && target_alpm ins openssh || :
	[ "$A_FIREWALL" = yes ] && target_alpm ins nftables || :

	step "Installing filesystem tools" "$A_FS"
	case "$A_FS" in
	btrfs) target_alpm ins btrfs-progs || : ;;
	xfs)   target_alpm ins xfsprogs || : ;;
	f2fs)  target_alpm ins f2fs-tools || : ;;
	zfs)   logit "zfs userspace tools are not packaged for Arctic yet" ;;
	*)     target_alpm ins e2fsprogs || : ;;  # ext4, ext3, ext2
	esac
	if [ "$A_ENCRYPT" = yes ]; then
		step "Installing cryptsetup" "LUKS"
		target_alpm ins cryptsetup || warn_soft "cryptsetup did not install"
	fi

	step "Setting up snapshots" "${A_SNAPSHOT:-none}"
	setup_snapshots

	step "Writing fstab" "/etc/fstab"
	write_fstab

	step "Writing crypttab" "/etc/crypttab"
	write_crypttab

	step "Setting up swap" "$A_SWAP"
	setup_swap

	step "Configuring locale" "$A_LOCALE"
	printf '%s UTF-8\n' "$A_LOCALE" >"$TARGET/etc/locale.gen"
	printf 'LANG=%s\nLC_COLLATE=C\n' "$A_LANG" >"$TARGET/etc/locale.conf"
	printf 'KEYMAP=%s\nFONT=arctic\n' "$A_KEYMAP" >"$TARGET/etc/vconsole.conf"
	chroot_run locale-gen 2>/dev/null || logit "locale-gen unavailable, skipped"

	step "Setting the time zone" "$A_TZ"
	if [ -f "$TARGET/usr/share/zoneinfo/$A_TZ" ]; then
		ln -sf "/usr/share/zoneinfo/$A_TZ" "$TARGET/etc/localtime"
	else
		logit "zoneinfo for $A_TZ missing on the target"
	fi
	printf '%s\n' "$A_TZ" >"$TARGET/etc/timezone"

	step "Setting the hostname" "$A_HOSTNAME"
	printf '%s\n' "$A_HOSTNAME" >"$TARGET/etc/hostname"
	cat >"$TARGET/etc/hosts" <<EOF
127.0.0.1	localhost
::1		localhost
127.0.1.1	$A_HOSTNAME.localdomain	$A_HOSTNAME
EOF

	step "Creating the user account" "$A_USER"
	create_users

	step "Configuring doas" "/etc/doas.conf"
	cat >"$TARGET/etc/doas.conf" <<EOF
# Arctic Linux - doas rules
# Members of the wheel group may act as root after entering their password.
permit persist :wheel
# Let anyone in wheel sync repositories without a prompt; it changes nothing.
permit nopass :wheel cmd alpm args fetch all
EOF
	chmod 0400 "$TARGET/etc/doas.conf"

	step "Configuring the network" "$A_NET"
	configure_network

	step "Enabling services" "init"
	enable_services

	step "Writing the Arctic release file" "/etc/arctic-release"
	write_release

	step "Generating the initramfs" "$A_KERNEL"
	chroot_run arctic-mkinitramfs || logit "initramfs generation skipped"

	step "Finishing up" "cleaning the cache"
	rm -rf "$TARGET/var/cache/alpm/build"/* 2>/dev/null || :
	# The first boot of a finished install shows the Arctic greeting.
	: >"$TARGET/var/lib/arctic/firstboot" 2>/dev/null || {
		mkdir -p "$TARGET/var/lib/arctic"; : >"$TARGET/var/lib/arctic/firstboot"; }
	sync

	tui_gauge "Done" 100
	sleep 1
}

# The single entry point: partition (if A_DISK asked for it), install, and
# wire up the bootloader - one command, nothing to pre-mount by hand.
run_install() {
	: >"$LOGF"
	total=24
	[ -n "$A_DISK" ] && total=$((total + 8))
	tui_gauge_init "$total"

	[ -n "$A_DISK" ] && partition_disk

	do_install

	if [ -n "$A_DISK" ]; then
		bootstrap=$(dirname "$0")/arctic-boot-strap
		[ -x "$bootstrap" ] || bootstrap=arctic-boot-strap
		if [ -n "$A_ESP" ]; then
			bootdir="$TARGET/boot"; bootflag="--efi"
		else
			# No ESP to speak of on BIOS - there is no separate /boot mount
			# for a BIOS layout, so hand it the root itself.
			bootdir="$TARGET"; bootflag="--bios"
		fi
		step "Installing the bootloader" "$A_BOOT_FW"
		if [ -n "$A_BOOTLOADER" ] && [ "$A_BOOT_FW" = bios ]; then
			set -- "$bootdir" "$bootflag" "--loader=$A_BOOTLOADER"
		else
			set -- "$bootdir" "$bootflag"
		fi
		if "$bootstrap" "$@" >>"$LOGF" 2>&1; then
			logit "bootloader installed"
		else
			warn_soft "arctic-boot-strap failed - see $LOGF, then run it by hand"
		fi
	fi
}

warn_soft() { logit "warning: $*"; }

# Snapshots only mean anything on btrfs, so everything here is conditional on it.
setup_snapshots() {
	[ "$A_FS" = btrfs ] || { logit "not btrfs, skipping snapshot setup"; return 0; }
	case "${A_SNAPSHOT:-none}" in none|"") logit "snapshots declined"; return 0 ;; esac

	mkdir -p "$TARGET/etc/arctic" "$TARGET/.snapshots"
	cat >"$TARGET/etc/arctic/snapshot.conf" <<-EOF
		# Written by arctic-strap.
		# Which tool arctic-snapshot hands over to. It drives btrfs directly
		# when that tool is not installed, which is what both of them do
		# underneath anyway.
		TOOL=$A_SNAPSHOT
		SUBVOL_DIR=/.snapshots
		# How many automatic snapshots to keep before the oldest are pruned.
		KEEP=20
	EOF

	# The real tool, if we have a package for it. alpm calls "arctic-snapshot
	# create" before every install and removal (see alpm/alpm's tx_snapshot),
	# and arctic-snapshot itself reads TOOL= above to decide who to delegate to.
	case "$A_SNAPSHOT" in
	timeshift) target_alpm ins timeshift 2>/dev/null || \
		logit "timeshift package not available; arctic-snapshot will drive btrfs directly" ;;
	snapper)   target_alpm ins snapper 2>/dev/null || \
		logit "snapper package not available; arctic-snapshot will drive btrfs directly" ;;
	esac
	logit "snapshots configured: $A_SNAPSHOT"
}

chroot_run() {
	[ -x "$TARGET/usr/bin/env" ] || [ -x "$TARGET/bin/sh" ] || return 1
	mount --bind /dev "$TARGET/dev" 2>/dev/null || :
	mount --bind /proc "$TARGET/proc" 2>/dev/null || :
	mount --bind /sys "$TARGET/sys" 2>/dev/null || :
	chroot "$TARGET" /bin/sh -lc "$*" >>"$LOGF" 2>&1
	rc=$?
	umount "$TARGET/dev" "$TARGET/proc" "$TARGET/sys" 2>/dev/null || :
	return $rc
}

write_fstab() {
	f="$TARGET/etc/fstab"
	{
		printf '# Arctic Linux - /etc/fstab\n'
		printf '# <device>\t<mount>\t<type>\t<options>\t<dump>\t<pass>\n'
		# Root: read it back from the mount table so we record what is real.
		rootdev=$(awk -v t="$TARGET" '$2==t{print $1}' /proc/mounts | tail -1)
		roottype=$(awk -v t="$TARGET" '$2==t{print $3}' /proc/mounts | tail -1)
		if [ -n "$rootdev" ]; then
			uuid=$(blkid -s UUID -o value "$rootdev" 2>/dev/null)
			dev=${uuid:+UUID=$uuid}; dev=${dev:-$rootdev}
			if [ "${roottype:-}" = btrfs ]; then
				# Every btrfs line needs its subvolume, or the machine mounts
				# the top level and the @ / @home split does nothing.
				bo=${A_BTRFS_OPTS:-rw,noatime,compress=zstd:3,space_cache=v2}
				printf '%s\t/\tbtrfs\t%s,subvol=@\t0\t0\n' "$dev" "$bo"
				printf '%s\t/home\tbtrfs\t%s,subvol=@home\t0\t0\n' "$dev" "$bo"
				printf '%s\t/.snapshots\tbtrfs\t%s,subvol=@snapshots\t0\t0\n' "$dev" "$bo"
				printf '%s\t/var/log\tbtrfs\t%s,subvol=@log\t0\t0\n' "$dev" "$bo"
			else
				opts="rw,relatime"
				[ "$(detect_rotational)" = ssd ] && opts="rw,noatime"
				printf '%s\t/\t%s\t%s\t0\t1\n' "$dev" "${roottype:-ext4}" "$opts"
			fi
		fi
		# Anything mounted under the target, boot included.
		awk -v t="$TARGET" 'index($2, t"/")==1 && $3!="btrfs" {print $1, substr($2, length(t)+1), $3}' \
			/proc/mounts | while read -r d m ty; do
			uuid=$(blkid -s UUID -o value "$d" 2>/dev/null)
			dd=${uuid:+UUID=$uuid}; dd=${dd:-$d}
			case "$ty" in
			vfat) printf '%s\t%s\tvfat\trw,relatime,fmask=0077,dmask=0077,utf8=1\t0\t2\n' "$dd" "$m" ;;
			*)    printf '%s\t%s\t%s\trw,relatime\t0\t2\n' "$dd" "$m" "$ty" ;;
			esac
		done
		printf 'tmpfs\t/tmp\ttmpfs\trw,nosuid,nodev,size=50%%\t0\t0\n'
		printf 'proc\t/proc\tproc\trw,nosuid,nodev,noexec\t0\t0\n'
		printf 'sysfs\t/sys\tsysfs\trw,nosuid,nodev,noexec\t0\t0\n'
		printf 'devpts\t/dev/pts\tdevpts\trw,nosuid,noexec,gid=5,mode=620\t0\t0\n'
	} >"$f"
}

# /etc/crypttab is the one thing arctic-mkinitramfs actually reads to decide
# whether to bundle cryptsetup and the dm-crypt/essiv modules at all - see
# that script for why. A_ROOTDEV is the raw partition auto_partition laid
# LUKS onto (kept separate from the /dev/mapper/cryptroot path that actually
# got formatted and mounted), so its LUKS UUID is what both this and the
# boot loader's kernel command line need.
write_crypttab() {
	[ "$A_ENCRYPT" = yes ] || return 0
	[ -n "$A_ROOTDEV" ] || { warn_soft "encryption chosen but no root device recorded"; return 0; }
	uuid=$(blkid -s UUID -o value "$A_ROOTDEV" 2>/dev/null)
	[ -n "$uuid" ] || { warn_soft "could not read the LUKS UUID of $A_ROOTDEV"; return 0; }
	printf 'cryptroot\tUUID=%s\tnone\tluks\n' "$uuid" >"$TARGET/etc/crypttab"
	chmod 600 "$TARGET/etc/crypttab"
}

setup_swap() {
	case "$A_SWAP" in
	none) return 0 ;;
	zram)
		mkdir -p "$TARGET/etc/arctic"
		printf 'ZRAM_SIZE=%s\nZRAM_ALGO=zstd\n' "$A_SWAPSIZE" >"$TARGET/etc/arctic/zram.conf"
		printf 'zram\n' >>"$TARGET/etc/modules-load.d/arctic.conf" 2>/dev/null || {
			mkdir -p "$TARGET/etc/modules-load.d"
			printf 'zram\n' >"$TARGET/etc/modules-load.d/arctic.conf"; }
		return 0 ;;
	partition)
		[ -n "$A_SWAPDEV" ] || return 0
		run mkswap -L arctic-swap "$A_SWAPDEV" || return 0
		uuid=$(blkid -s UUID -o value "$A_SWAPDEV" 2>/dev/null)
		printf '%s\tnone\tswap\tsw,pri=10\t0\t0\n' "${uuid:+UUID=$uuid}${uuid:-$A_SWAPDEV}" \
			>>"$TARGET/etc/fstab"
		return 0 ;;
	file)
		sz=$(printf '%s' "$A_SWAPSIZE" | sed 's/[Gg]$//')
		mb=$(( ${sz:-4} * 1024 ))
		logit "creating a ${mb}MiB swap file"
		# fallocate leaves holes on some filesystems, so write it out.
		if ! run fallocate -l "${mb}M" "$TARGET/swapfile"; then
			run dd if=/dev/zero of="$TARGET/swapfile" bs=1M count="$mb"
		fi
		chmod 600 "$TARGET/swapfile"
		run mkswap "$TARGET/swapfile" || return 0
		printf '/swapfile\tnone\tswap\tsw,pri=10\t0\t0\n' >>"$TARGET/etc/fstab"
		return 0 ;;
	esac
}

create_users() {
	# Groups first. Arctic uses wheel for administrative access.
	for g in wheel audio video input storage network render kvm; do
		chroot_run "grep -q '^$g:' /etc/group || addgroup -S $g" || :
	done
	# dbus-daemon --system drops root to this user once it has bound the bus
	# socket - see /usr/share/dbus-1/system.conf. Without the account it
	# cannot start at all, which is why plain iwd (needs the system bus to
	# register itself) used to fail with "failed to initialize dbus".
	chroot_run "grep -q '^messagebus:' /etc/group || addgroup -S messagebus" || :
	chroot_run "grep -q '^messagebus:' /etc/passwd || \
		adduser -S -H -D -G messagebus -s /sbin/nologin messagebus" || :
	if [ -n "$A_USER" ]; then
		chroot_run "adduser -D -s $A_SHELL -h /home/$A_USER $A_USER" || \
			chroot_run "useradd -m -s $A_SHELL $A_USER" || :
		for g in wheel audio video input storage network render; do
			chroot_run "addgroup $A_USER $g 2>/dev/null || usermod -aG $g $A_USER" || :
		done
		if [ -n "$A_USERPASS" ]; then
			printf '%s:%s\n' "$A_USER" "$A_USERPASS" | \
				chroot "$TARGET" chpasswd >>"$LOGF" 2>&1 || \
				logit "could not set the password for $A_USER"
		fi
		# Drop the Arctic shell configuration into the new home.
		for f in .zshrc .zshenv .zprofile; do
			[ -f "$TARGET/etc/skel/$f" ] && \
				cp -f "$TARGET/etc/skel/$f" "$TARGET/home/$A_USER/$f"
		done
		chroot_run "chown -R $A_USER:$A_USER /home/$A_USER" || :
	fi
	if [ "$A_ROOTPASS" = "!locked" ]; then
		chroot_run "passwd -l root" || :
		logit "root account locked"
	elif [ -n "$A_ROOTPASS" ]; then
		printf 'root:%s\n' "$A_ROOTPASS" | \
			chroot "$TARGET" chpasswd >>"$LOGF" 2>&1 || logit "could not set the root password"
	fi
}

configure_network() {
	mkdir -p "$TARGET/etc/arctic"
	case "$A_NET" in
	wifi)
		# iwd keeps one file per network; write it so the link comes back up.
		if [ -n "$A_WIFI_SSID" ]; then
			mkdir -p "$TARGET/var/lib/iwd"
			printf '[Security]\nPassphrase=%s\n' "$A_WIFI_PSK" \
				>"$TARGET/var/lib/iwd/$A_WIFI_SSID.psk"
			chmod 600 "$TARGET/var/lib/iwd/$A_WIFI_SSID.psk"
		fi
		{
			printf 'NET_MODE=wifi\nNET_SSID=%s\n' "$A_WIFI_SSID"
			# rc.d/network's pick_iface() only trusts NET_IFACE when it is
			# set; without it, a machine with more than one interface can
			# come up on the wrong one after a wifi-only install.
			[ -n "$A_WIFI_IFACE" ] && printf 'NET_IFACE=%s\n' "$A_WIFI_IFACE"
		} >"$TARGET/etc/arctic/network.conf"
		;;
	static:*)
		iface=$(printf '%s' "$A_NET" | cut -d: -f2)
		addr=$(printf '%s' "$A_NET" | cut -d: -f3)
		gw=$(printf '%s' "$A_NET" | cut -d: -f4)
		dns=$(printf '%s' "$A_NET" | cut -d: -f5)
		cat >"$TARGET/etc/arctic/network.conf" <<EOF
NET_MODE=static
NET_IFACE=$iface
NET_ADDR=$addr
NET_GATEWAY=$gw
NET_DNS=$dns
EOF
		printf 'nameserver %s\n' "$dns" >"$TARGET/etc/resolv.conf"
		;;
	*)
		printf 'NET_MODE=dhcp\n' >"$TARGET/etc/arctic/network.conf"
		[ -f "$TARGET/etc/resolv.conf" ] || \
			printf 'nameserver 1.1.1.1\nnameserver 9.9.9.9\n' >"$TARGET/etc/resolv.conf"
		;;
	esac
}

enable_services() {
	svc() {
		mkdir -p "$TARGET/etc/arctic/services"
		: >"$TARGET/etc/arctic/services/$1"
		logit "enabled service $1"
	}
	svc udev
	svc network
	svc dbus
	[ "$A_NET" = wifi ] && svc iwd
	[ "$A_NTP" = yes ] && svc ntpd
	[ "$A_SSH" = yes ] && svc sshd
	[ "$A_FIREWALL" = yes ] && svc nftables
	[ "$A_BLUETOOTH" = yes ] && svc bluetoothd
	[ "$A_TRIM" = yes ] && [ "$(detect_rotational)" = ssd ] && svc fstrim
	case "${A_DM:-none}" in
	sddm) svc sddm ;;
	xdm) svc xdm ;;
	greetd) svc greetd ;;
	esac
	[ "$A_AUDIO" = yes ] && svc pipewire
	:
}

write_release() {
	cat >"$TARGET/etc/arctic-release" <<EOF
NAME="Arctic Linux"
PRETTY_NAME="Arctic Linux"
ID=arctic
BUILD_ID=$(date '+%Y.%m.%d')
KERNEL=$A_KERNEL
INSTALLED=$(date '+%Y-%m-%d %H:%M:%S')
INSTALLER=arctic-strap $VERSION
LIBC=glibc
TOOLCHAIN=llvm
USERLAND=bsd
INIT=busybox
HOME_URL="https://github.com/apiwo/arctic-linux"
EOF
	ln -sf /etc/arctic-release "$TARGET/etc/os-release" 2>/dev/null || \
		cp "$TARGET/etc/arctic-release" "$TARGET/etc/os-release"
	printf 'Arctic Linux \\r (\\l)\n\n' >"$TARGET/etc/issue"
}

finished_banner() {
	if [ -n "$A_DISK" ]; then
		cat <<EOF

$(printf '%s' "$c_teal$t_bold")  Arctic Linux is installed on $A_DISK, bootloader included.$(printf '%s' "$t_reset")

  To change anything inside the new system first:

      $(printf '%s' "$c_snow")arctic-chroot$(printf '%s' "$t_reset")

  Otherwise:

      $(printf '%s' "$c_snow")reboot$(printf '%s' "$t_reset")

EOF
	else
		cat <<EOF

$(printf '%s' "$c_teal$t_bold")  Arctic Linux is installed.$(printf '%s' "$t_reset")

  Next: mount the boot partition and install the bootloader.

      $(printf '%s' "$c_snow")mount /dev/nvme0n1p2 $TARGET/boot$(printf '%s' "$t_reset")
      $(printf '%s' "$c_snow")arctic-boot-strap $TARGET/boot --efi$(printf '%s' "$t_reset")   (or --bios)

  To change anything inside the new system first:

      $(printf '%s' "$c_snow")arctic-chroot$(printf '%s' "$t_reset")

  Otherwise:

      $(printf '%s' "$c_snow")reboot$(printf '%s' "$t_reset")

EOF
	fi
}

# ----------------------------------------------------------------------- main

need_root 2>/dev/null || { echo "Must be run as root." >&2; exit 1; }

FORCE=""
CFFILE="$CONF"
ARGTARGET=""
while [ $# -gt 0 ]; do
	case "$1" in
	-c|--config) shift; CFFILE=${1:?usage: arctic-strap -c <file> [<target>]}; shift ;;
	-f|--force)  FORCE=1; shift ;;
	-h|--help|help)
		sed -n '2,25p' "$0" | sed 's/^# \?//'; exit 0 ;;
	--version)
		printf 'arctic-strap %s\n' "$VERSION"; exit 0 ;;
	-*) echo "arctic-strap: unknown option $1" >&2; exit 1 ;;
	*)
		# TARGET can come before or after -c/-f (typing the target first,
		# then a flag, is just as natural as the documented order) - a plain
		# "break" here on the first non-flag argument would stop the loop
		# before it ever saw a flag that came after TARGET, silently
		# discarding it.
		if [ -z "$ARGTARGET" ]; then ARGTARGET=$1; shift
		else echo "arctic-strap: unexpected argument: $1" >&2; exit 1
		fi ;;
	esac
done

# The dot builtin searches PATH when given a bare filename, so a relative path
# like "install.conf" is not found. Make it absolute first.
case "$CFFILE" in
/*) ;;
*) CFFILE="$(pwd)/$CFFILE" ;;
esac
if [ -f "$CFFILE" ]; then
	# shellcheck disable=SC1090
	. "$CFFILE"
elif [ "$CFFILE" != "$CONF" ]; then
	bail "no such config file: $CFFILE"
else
	echo "arctic-strap: no config at $CONF - installing with built-in defaults" >&2
fi

# The command line always wins over whatever the config file set. TARGET is
# only something the manual (A_DISK unset) path needs to think about at all;
# when A_DISK is set this is just where partition_disk mounts things, created
# if it does not exist yet, so /mnt as a fallback is fine either way.
[ -n "$ARGTARGET" ] && TARGET=$ARGTARGET
[ -n "$TARGET" ] || TARGET=/mnt
case "$TARGET" in
/*) ;;
*) TARGET="$(pwd)/$TARGET" ;;
esac
TARGET=$(printf '%s' "$TARGET" | sed 's|/*$||')

if [ -z "$A_DISK" ]; then
	[ -d "$TARGET" ] || bail "$TARGET does not exist. Either mount a target yourself first:
  mount /dev/nvme0n1p1 /mnt
or set A_DISK in the config to partition one automatically."
fi

if [ -z "$FORCE" ] && detect_existing_install "$TARGET"; then
	printf '\n\033[38;5;215mThere is already an Arctic install on %s.\033[0m\n' "$TARGET"
	printf 'Continuing will overwrite files that belong to packages.\n\n'
	printf 'Continue? [y/N] '
	read -r a
	case "$a" in y|Y|yes) ;; *) echo "nothing was changed."; exit 0 ;; esac
fi

tui_init
run_install
tui_done
finished_banner
