Splux Linux

source-based Linux using SPS

SPS · log · files · refs · tags · releases · people · languages · GitHub · file

bin/setup


Shell

69775 bytes at 2909919 · GitHub

#!/bin/sh
# SPS Linux installer (informally Splux). Interactive screens use dialog(1);
# every choice is also available as a flag for scripts and tests.
# This is the distro installer, not a generic LFS helper. SPS the package
# system still works on other trees without running setup.

sps_prog=setup
sps_bin_dir=$(CDPATH= cd -P "$(dirname "$0")" 2>/dev/null && pwd) || exit 1
if [ -z "${SPS_LIBDIR+x}" ]; then
	if [ -r "$sps_bin_dir/../lib/common.sh" ]; then
		SPS_LIBDIR=$sps_bin_dir/../lib
	else
		SPS_LIBDIR=$sps_bin_dir/../lib/sps
	fi
fi
if ! . "$SPS_LIBDIR/common.sh"; then
	printf '%s\n' "setup: cannot load $SPS_LIBDIR/common.sh" >&2
	exit 1
fi
# Remember caller-supplied paths. sps_load_config exports defaults for the
# running system; after --target those must not leak into src/sget.
[ "${SPS_DB+x}" = x ] && setup_keep_db=1 || setup_keep_db=0
[ "${SPS_CACHE+x}" = x ] && setup_keep_cache=1 || setup_keep_cache=0
[ "${SPS_BUILD+x}" = x ] && setup_keep_build=1 || setup_keep_build=0
[ "${SPS_REPO_ROOT+x}" = x ] && setup_keep_repo_root=1 || setup_keep_repo_root=0
[ "${SPS_CONFIG+x}" = x ] && setup_keep_config=1 || setup_keep_config=0
[ "${SPS_REPOS_CONFIG+x}" = x ] && setup_keep_repos=1 || setup_keep_repos=0
sps_load_config || exit $?

setup_usage()
{
	cat <<'EOF'
usage: setup [options]

Install SPS Linux (Splux) into a target root using dialog(1) screens, or
the same choices from the command line. This is the distro installer.
SPS the package system can be used without it (for example on LFS).

Disks: guided layouts erase the chosen disk after two confirmations.
--plan and --non-interactive never touch a disk unless you pass --disk
and --format yes together. grub-install still needs --install-bootloader
and --disk; setup will not guess a target.

options:
  --target DIR              installation root (SPS_ROOT)
  --profile NAME            minimal, server, plasma-desktop, or plasma-full
  --enable SET[,SET...]     extra sets (repeatable)
  --disable SET[,SET...]    extra sets to leave off (repeatable)
  --hostname NAME
  --timezone ZONE           e.g. UTC or America/Los_Angeles
  --keymap MAP              e.g. us, uk, de, dvorak
  --locale NAME             e.g. C.UTF-8 or en_US.UTF-8
  --shell PATH              login shell for the first user
  --user NAME               first unprivileged user (uid 1000)
  --user-gecos TEXT
  --user-password-file FILE hash the first line as the user password
  --root-password-file FILE hash the first line as the root password
  --user-password-hash HASH already-hashed user shadow field
  --root-password-hash HASH already-hashed root shadow field
  --extra PKG[,PKG...]      individual packages in addition to sets
  --ssh yes|no              include the ssh set (OpenSSH)
  --printing yes|no         include the printing set (CUPS)
  --flatpak yes|no          add Flathub after install when flatpak is present
  --nvidia yes|no           proprietary NVIDIA set (off by default; live uses nouveau)
  --partition LAYOUT        none, existing, efi-root, efi-swap-root,
                            efi-swap-root-home, bios-root, manage
  --disk DEV                whole disk for guided layouts (e.g. /dev/sda)
  --swap-size SIZE          sfdisk size for swap (default 8GiB)
  --root-part DEV           root partition for --partition manage
  --efi-part DEV            EFI partition for --partition manage
  --swap-part DEV           swap partition for --partition manage
  --home-part DEV           /home partition for --partition manage
  --format yes|no           required yes for non-interactive guided layouts
  --services yes|no         enable known services for the chosen init (default yes)
  --kernel NAME             none, linux, linux-desktop, or custom
  --kernel-config FILE      .config for --kernel custom
  --drivers auto|none       map lspci/cpu to packages (microcode, nouveau, iwd)
  --init systemd|openrc|s6|runit|dinit|shepherd|sysvinit|none
                            PID 1 on the installed system (default: none in
                            scripts; live /etc/sps/init or systemd on the
                            interactive screen)
  --bootloader none|grub    write bootloader notes; never implied
  --install-bootloader      run grub-install on --disk (explicit, dangerous)
  --update|--no-update      run or skip src update before installing
  --non-interactive         do not start dialog; all choices from options
  --plan                    print the package list and exit
  --write-answers FILE      write the resolved answers
  --read-answers FILE       load answers (options override file values)
  --list-profiles
  --list-sets
  --list-hardware           print detected devices and suggested packages
  -h, --help
EOF
}

setup_dir=${SPS_SETUP_DIR:-$SPS_LIBDIR/setup}
if [ -f "$SPS_LIBDIR/setup/partition.sh" ]; then
	# shellcheck disable=SC1091
	. "$SPS_LIBDIR/setup/partition.sh"
fi
if [ -f "$SPS_LIBDIR/setup/hwdetect.sh" ]; then
	# shellcheck disable=SC1091
	. "$SPS_LIBDIR/setup/hwdetect.sh"
fi
if [ -f "$SPS_LIBDIR/setup/service.sh" ]; then
	# shellcheck disable=SC1091
	. "$SPS_LIBDIR/setup/service.sh"
fi

# True when package $2 cannot be installed with chosen init $1.
setup_pkg_conflicts_init()
{
	setup_ci=$1
	setup_cp=$2
	case $setup_ci in
		systemd)
			case $setup_cp in
				elogind|eudev|openrc|init-openrc|s6|s6-linux-init|init-s6|runit|init-runit|dinit|init-dinit|shepherd|init-shepherd|sysvinit|init-sysvinit)
					return 0
					;;
			esac
			;;
		openrc)
			case $setup_cp in
				systemd|init-systemd|s6|s6-linux-init|init-s6|runit|init-runit|dinit|init-dinit|shepherd|init-shepherd|sysvinit|init-sysvinit)
					return 0
					;;
			esac
			;;
		s6)
			case $setup_cp in
				systemd|init-systemd|openrc|init-openrc|runit|init-runit|dinit|init-dinit|shepherd|init-shepherd|sysvinit|init-sysvinit)
					return 0
					;;
			esac
			;;
		runit)
			case $setup_cp in
				systemd|init-systemd|openrc|init-openrc|s6|s6-linux-init|init-s6|dinit|init-dinit|shepherd|init-shepherd|sysvinit|init-sysvinit)
					return 0
					;;
			esac
			;;
		dinit)
			case $setup_cp in
				systemd|init-systemd|openrc|init-openrc|s6|s6-linux-init|init-s6|runit|init-runit|shepherd|init-shepherd|sysvinit|init-sysvinit)
					return 0
					;;
			esac
			;;
		shepherd)
			case $setup_cp in
				systemd|init-systemd|openrc|init-openrc|s6|s6-linux-init|init-s6|runit|init-runit|dinit|init-dinit|sysvinit|init-sysvinit)
					return 0
					;;
			esac
			;;
		sysvinit)
			case $setup_cp in
				systemd|init-systemd|openrc|init-openrc|s6|s6-linux-init|init-s6|runit|init-runit|dinit|init-dinit|shepherd|init-shepherd)
					return 0
					;;
			esac
			;;
	esac
	return 1
}

setup_collect_recipe_names()
{
	setup_names_out=$1
	: >"$setup_names_out"
	for setup_rtree in \
		/usr/src/sps/core \
		/usr/src/sps/extra \
		"${SPS_LIVE_ROOT:-/}/usr/src/sps/core" \
		"${SPS_LIVE_ROOT:-/}/usr/src/sps/extra"
	do
		[ -d "$setup_rtree" ] || continue
		find "$setup_rtree" \( -type d \( -name .git -o -name files -o \
			-name patches -o -name hooks \) -prune \) -o \
			-type f -name recipe -print
	done | while IFS= read -r setup_rfile || [ -n "$setup_rfile" ]
	do
		[ -n "$setup_rfile" ] || continue
		awk '/^name[[:space:]]/ { print $2; exit }' "$setup_rfile"
	done | LC_ALL=C sort -u >"$setup_names_out"
}

setup_reject_unknown_packages()
{
	setup_index=${SPS_CACHE:-$setup_root/var/cache/sps}/indexes/packages.index
	[ -f "$setup_index" ] || return 0
	[ -s "$setup_pkg_file" ] || return 0
	setup_unknown=$setup_work/unknown-packages
	awk -F '\t' '
		FNR == NR {
			gsub(/\r/, "")
			if ($1 != "")
				want[$1] = 1
			next
		}
		{ have[$1] = 1 }
		END {
			for (n in want)
				if (!(n in have))
					print n
		}
	' "$setup_pkg_file" "$setup_index" | LC_ALL=C sort >"$setup_unknown"
	[ -s "$setup_unknown" ] || return 0
	printf '%s: packages not in the recipe index:\n' "$sps_prog" >&2
	sed 's/^/  /' "$setup_unknown" >&2
	setup_fail "$SPS_EX_NOTFOUND" \
		"requested packages are not packaged (see the list above)"
}

setup_work=
setup_interactive=1
setup_plan=0
setup_list_profiles=0
setup_list_sets=0
setup_do_update=1
setup_install_bootloader=0
setup_target=
setup_profile=
setup_hostname=
setup_timezone=
setup_keymap=
setup_locale=
setup_shell=
setup_user=
setup_user_gecos=
setup_user_password_hash=
setup_root_password_hash=
setup_extra=
setup_ssh=
setup_printing=
setup_flatpak=
setup_nvidia=
setup_bootloader=none
setup_answers_in=
setup_answers_out=
setup_enable=
setup_disable=
setup_partition=
setup_disk=
setup_swap_size=8GiB
setup_format=
setup_kernel=
setup_kernel_config=
setup_drivers=
setup_init=
setup_services=
setup_part_efi=
setup_part_root=
setup_part_swap=
setup_part_home=
setup_part_bios=
setup_list_hardware=0

setup_cleanup()
{
	if [ -n "$setup_work" ] && [ -d "$setup_work" ]; then
		rm -rf "$setup_work"
	fi
}

setup_fail()
{
	setup_code=$1
	shift
	printf '%s: %s\n' "$sps_prog" "$*" >&2
	exit "$setup_code"
}

setup_need_value()
{
	[ "$#" -ge 2 ] && [ -n "${2-}" ] ||
		setup_fail "$SPS_EX_USAGE" "option '$1' requires an argument"
}

setup_append_csv()
{
	setup_dest=$1
	setup_csv=$2
	setup_old=$3
	setup_item=
	setup_rest=$setup_csv,
	while [ -n "$setup_rest" ]; do
		setup_item=${setup_rest%%,*}
		setup_rest=${setup_rest#*,}
		[ -n "$setup_item" ] || continue
		case " $setup_old " in
			*" $setup_item "*) ;;
			*) setup_old="$setup_old $setup_item" ;;
		esac
	done
	eval "$setup_dest=\"\$(printf '%s' \"\$setup_old\" | sed 's/^ //')\""
}

setup_hash_password_stdin()
{
	if command -v openssl >/dev/null 2>&1; then
		openssl passwd -6 -stdin
		return $?
	fi
	if command -v python3 >/dev/null 2>&1; then
		python3 -c 'import crypt,sys
p=sys.stdin.read().rstrip("\n")
print(crypt.crypt(p, crypt.mksalt(crypt.METHOD_SHA512)))'
		return $?
	fi
	if command -v busybox >/dev/null 2>&1; then
		busybox cryptpw -m sha512 "$(cat)"
		return $?
	fi
	return 1
}

setup_hash_password_file()
{
	setup_pwf=$1
	[ -f "$setup_pwf" ] && [ -r "$setup_pwf" ] ||
		setup_fail "$SPS_EX_NOTFOUND" "cannot read password file: $setup_pwf"
	setup_pw=$(sed -n '1p' "$setup_pwf")
	[ -n "$setup_pw" ] ||
		setup_fail "$SPS_EX_USAGE" "password file is empty: $setup_pwf"
	printf '%s\n' "$setup_pw" | setup_hash_password_stdin
}

setup_shadow_set_hash()
{
	setup_sh_file=$1
	setup_sh_user=$2
	setup_sh_hash=$3
	[ -f "$setup_sh_file" ] || return 0
	[ -n "$setup_sh_hash" ] || return 0
	setup_sh_tmp=$setup_sh_file.tmp
	setup_u=$setup_sh_user setup_h=$setup_sh_hash awk -F: '
		BEGIN { OFS = ":" }
		$1 == ENVIRON["setup_u"] { $2 = ENVIRON["setup_h"] }
		{ print }
	' "$setup_sh_file" >"$setup_sh_tmp" || return 1
	mv "$setup_sh_tmp" "$setup_sh_file"
	chmod 0640 "$setup_sh_file" 2>/dev/null || chmod 0600 "$setup_sh_file" || :
}

setup_group_ensure()
{
	setup_gf=$1
	setup_gn=$2
	setup_gid=$3
	[ -f "$setup_gf" ] || return 0
	grep -q "^$setup_gn:" "$setup_gf" && return 0
	printf '%s:x:%s:\n' "$setup_gn" "$setup_gid" >>"$setup_gf"
}

setup_group_add_user()
{
	setup_gf=$1
	setup_gn=$2
	setup_gu=$3
	[ -f "$setup_gf" ] || return 0
	grep -q "^$setup_gn:" "$setup_gf" || return 0
	setup_g_tmp=$setup_gf.tmp
	setup_g=$setup_gn setup_u=$setup_gu awk -F: '
		BEGIN { OFS = ":" }
		$1 == ENVIRON["setup_g"] {
			if ($4 == "") $4 = ENVIRON["setup_u"]
			else {
				n = split($4, a, ",")
				found = 0
				for (i = 1; i <= n; i++) if (a[i] == ENVIRON["setup_u"]) found = 1
				if (!found) $4 = $4 "," ENVIRON["setup_u"]
			}
		}
		{ print }
	' "$setup_gf" >"$setup_g_tmp" || return 1
	mv "$setup_g_tmp" "$setup_gf"
}

setup_ask_password_pair()
{
	setup_pw_title=$1
	setup_pw_prompt=$2
	setup_p1=$(setup_dialog --title "$setup_pw_title" \
		--passwordbox "$setup_pw_prompt" 9 50) ||
		setup_fail "$SPS_EX_USAGE" "cancelled"
	setup_p2=$(setup_dialog --title "$setup_pw_title" \
		--passwordbox "Repeat the password." 9 50) ||
		setup_fail "$SPS_EX_USAGE" "cancelled"
	if [ "$setup_p1" != "$setup_p2" ]; then
		setup_fail "$SPS_EX_USAGE" "passwords do not match"
	fi
	if [ -z "$setup_p1" ]; then
		printf '%s\n' ''
		return 0
	fi
	printf '%s\n' "$setup_p1" | setup_hash_password_stdin ||
		setup_fail "$SPS_EX_PACKAGE" \
			"cannot hash passwords (need openssl passwd, python3, or busybox cryptpw)"
}

while [ "$#" -gt 0 ]; do
	case $1 in
		-h|--help)
			setup_usage
			exit 0
			;;
		--target)
			setup_need_value "$@"
			setup_target=$2
			shift 2
			;;
		--profile)
			setup_need_value "$@"
			setup_profile=$2
			shift 2
			;;
		--enable)
			setup_need_value "$@"
			setup_append_csv setup_enable "$2" "$setup_enable"
			shift 2
			;;
		--disable)
			setup_need_value "$@"
			setup_append_csv setup_disable "$2" "$setup_disable"
			shift 2
			;;
		--hostname)
			setup_need_value "$@"
			setup_hostname=$2
			shift 2
			;;
		--timezone)
			setup_need_value "$@"
			setup_timezone=$2
			shift 2
			;;
		--keymap)
			setup_need_value "$@"
			setup_keymap=$2
			shift 2
			;;
		--locale)
			setup_need_value "$@"
			setup_locale=$2
			shift 2
			;;
		--shell)
			setup_need_value "$@"
			setup_shell=$2
			shift 2
			;;
		--extra)
			setup_need_value "$@"
			setup_append_csv setup_extra "$2" "$setup_extra"
			shift 2
			;;
		--ssh)
			setup_need_value "$@"
			setup_ssh=$2
			shift 2
			;;
		--printing)
			setup_need_value "$@"
			setup_printing=$2
			shift 2
			;;
		--user)
			setup_need_value "$@"
			setup_user=$2
			shift 2
			;;
		--user-gecos)
			setup_need_value "$@"
			setup_user_gecos=$2
			shift 2
			;;
		--user-password-file)
			setup_need_value "$@"
			setup_user_password_hash=$(setup_hash_password_file "$2") ||
				setup_fail "$SPS_EX_PACKAGE" "cannot hash $2"
			[ -n "$setup_user_password_hash" ] ||
				setup_fail "$SPS_EX_USAGE" "user password hash was empty"
			shift 2
			;;
		--root-password-file)
			setup_need_value "$@"
			setup_root_password_hash=$(setup_hash_password_file "$2") ||
				setup_fail "$SPS_EX_PACKAGE" "cannot hash $2"
			[ -n "$setup_root_password_hash" ] ||
				setup_fail "$SPS_EX_USAGE" "root password hash was empty"
			shift 2
			;;
		--user-password-hash)
			setup_need_value "$@"
			setup_user_password_hash=$2
			shift 2
			;;
		--root-password-hash)
			setup_need_value "$@"
			setup_root_password_hash=$2
			shift 2
			;;
		--flatpak)
			setup_need_value "$@"
			setup_flatpak=$2
			shift 2
			;;
		--nvidia)
			setup_need_value "$@"
			setup_nvidia=$2
			shift 2
			;;
		--partition)
			setup_need_value "$@"
			setup_partition=$2
			shift 2
			;;
		--disk)
			setup_need_value "$@"
			setup_disk=$2
			shift 2
			;;
		--swap-size)
			setup_need_value "$@"
			setup_swap_size=$2
			shift 2
			;;
		--root-part)
			setup_need_value "$@"
			setup_part_root=$2
			shift 2
			;;
		--efi-part)
			setup_need_value "$@"
			setup_part_efi=$2
			shift 2
			;;
		--swap-part)
			setup_need_value "$@"
			setup_part_swap=$2
			shift 2
			;;
		--home-part)
			setup_need_value "$@"
			setup_part_home=$2
			shift 2
			;;
		--format)
			setup_need_value "$@"
			setup_format=$2
			shift 2
			;;
		--kernel)
			setup_need_value "$@"
			setup_kernel=$2
			shift 2
			;;
		--kernel-config)
			setup_need_value "$@"
			setup_kernel_config=$2
			shift 2
			;;
		--drivers)
			setup_need_value "$@"
			setup_drivers=$2
			shift 2
			;;
		--init)
			setup_need_value "$@"
			setup_init=$2
			shift 2
			;;
		--services)
			setup_need_value "$@"
			setup_services=$2
			shift 2
			;;
		--list-hardware)
			setup_list_hardware=1
			shift
			;;
		--bootloader)
			setup_need_value "$@"
			setup_bootloader=$2
			shift 2
			;;
		--install-bootloader)
			setup_install_bootloader=1
			shift
			;;
		--update)
			setup_do_update=1
			shift
			;;
		--no-update)
			setup_do_update=0
			shift
			;;
		--non-interactive)
			setup_interactive=0
			shift
			;;
		--plan)
			setup_plan=1
			setup_interactive=0
			shift
			;;
		--write-answers)
			setup_need_value "$@"
			setup_answers_out=$2
			shift 2
			;;
		--read-answers)
			setup_need_value "$@"
			setup_answers_in=$2
			shift 2
			;;
		--list-profiles)
			setup_list_profiles=1
			shift
			;;
		--list-sets)
			setup_list_sets=1
			shift
			;;
		--)
			shift
			break
			;;
		-*)
			setup_fail "$SPS_EX_USAGE" "unknown option: $1"
			;;
		*)
			setup_fail "$SPS_EX_USAGE" "unexpected argument: $1"
			;;
	esac
done

[ "$#" -eq 0 ] || setup_fail "$SPS_EX_USAGE" "unexpected argument: $1"

setup_field()
{
	setup_file=$1
	setup_key=$2
	awk -v k="$setup_key" '
		$1 == k {
			$1 = ""
			sub(/^[ \t]+/, "")
			print
			exit
		}
	' "$setup_file"
}

setup_packages_from()
{
	awk '
		$1 == "package" {
			$1 = ""
			sub(/^[ \t]+/, "")
			if ($0 != "") print
		}
	' "$1"
}

setup_profile_file()
{
	printf '%s/profiles/%s.profile' "$setup_dir" "$1"
}

setup_set_file()
{
	printf '%s/sets/%s.set' "$setup_dir" "$1"
}

setup_each_profile()
{
	setup_p=
	for setup_p in "$setup_dir"/profiles/*.profile; do
		[ -f "$setup_p" ] || continue
		setup_field "$setup_p" name
	done
}

setup_each_set()
{
	setup_s=
	for setup_s in "$setup_dir"/sets/*.set; do
		[ -f "$setup_s" ] || continue
		setup_field "$setup_s" name
	done
}

setup_valid_profile()
{
	[ -f "$(setup_profile_file "$1")" ]
}

setup_valid_set()
{
	[ -f "$(setup_set_file "$1")" ]
}

setup_default_sets_for_profile()
{
	setup_ds=
	setup_sf=
	for setup_sf in "$setup_dir"/sets/*.set; do
		[ -f "$setup_sf" ] || continue
		setup_sn=$(setup_field "$setup_sf" name)
		setup_def=$(setup_field "$setup_sf" default)
		[ "$setup_def" = 1 ] || continue
		case $setup_profile in
			plasma-desktop|plasma-full)
				setup_ds="$setup_ds $setup_sn"
				;;
			*)
				# Power and firmware default on for desktops only.
				[ "$setup_sn" = power ] && continue
				[ "$setup_sn" = firmware ] && continue
				[ "$setup_sn" = qol-cli ] &&
					setup_ds="$setup_ds $setup_sn"
				;;
		esac
	done
	printf '%s' "$setup_ds" | sed 's/^ //'
}

if [ "$setup_list_profiles" -eq 1 ]; then
	for setup_pf in "$setup_dir"/profiles/*.profile; do
		[ -f "$setup_pf" ] || continue
		printf '%s\t%s\t%s\n' \
			"$(setup_field "$setup_pf" name)" \
			"$(setup_field "$setup_pf" title)" \
			"$(setup_field "$setup_pf" description)"
	done
	exit 0
fi

if [ "$setup_list_sets" -eq 1 ]; then
	for setup_sf in "$setup_dir"/sets/*.set; do
		[ -f "$setup_sf" ] || continue
		printf '%s\t%s\t%s\n' \
			"$(setup_field "$setup_sf" name)" \
			"$(setup_field "$setup_sf" title)" \
			"$(setup_field "$setup_sf" description)"
	done
	exit 0
fi

if [ "$setup_list_hardware" -eq 1 ]; then
	if command -v setup_hw_summary >/dev/null 2>&1; then
		setup_hw_summary
		printf '\nSuggested packages:\n'
		setup_hw_packages | sort -u
	else
		setup_fail "$SPS_EX_NOTFOUND" "hardware detection is not available"
	fi
	exit 0
fi

setup_load_answers()
{
	setup_af=$1
	[ -f "$setup_af" ] && [ -r "$setup_af" ] ||
		setup_fail "$SPS_EX_NOTFOUND" "cannot read answers file: $setup_af"
	while IFS= read -r setup_line || [ -n "$setup_line" ]; do
		case $setup_line in
			''|\#*) continue ;;
		esac
		setup_ak=${setup_line%% *}
		setup_av=${setup_line#* }
		[ "$setup_ak" = "$setup_line" ] && setup_av=
		case $setup_ak in
			target) [ -n "$setup_target" ] || setup_target=$setup_av ;;
			profile) [ -n "$setup_profile" ] || setup_profile=$setup_av ;;
			hostname) [ -n "$setup_hostname" ] || setup_hostname=$setup_av ;;
			timezone) [ -n "$setup_timezone" ] || setup_timezone=$setup_av ;;
			keymap) [ -n "$setup_keymap" ] || setup_keymap=$setup_av ;;
			locale) [ -n "$setup_locale" ] || setup_locale=$setup_av ;;
			shell) [ -n "$setup_shell" ] || setup_shell=$setup_av ;;
			user) [ -n "$setup_user" ] || setup_user=$setup_av ;;
			user-gecos) [ -n "$setup_user_gecos" ] || setup_user_gecos=$setup_av ;;
			user-password-hash)
				[ -n "$setup_user_password_hash" ] ||
					setup_user_password_hash=$setup_av
				;;
			root-password-hash)
				[ -n "$setup_root_password_hash" ] ||
					setup_root_password_hash=$setup_av
				;;
			extra) setup_append_csv setup_extra "$setup_av" "$setup_extra" ;;
			ssh) [ -n "$setup_ssh" ] || setup_ssh=$setup_av ;;
			printing) [ -n "$setup_printing" ] || setup_printing=$setup_av ;;
			flatpak) [ -n "$setup_flatpak" ] || setup_flatpak=$setup_av ;;
			nvidia) [ -n "$setup_nvidia" ] || setup_nvidia=$setup_av ;;
			partition) [ -n "$setup_partition" ] || setup_partition=$setup_av ;;
			disk) [ -n "$setup_disk" ] || setup_disk=$setup_av ;;
			swap-size) [ "$setup_swap_size" = 8GiB ] && [ -n "$setup_av" ] && setup_swap_size=$setup_av ;;
			root-part) [ -n "$setup_part_root" ] || setup_part_root=$setup_av ;;
			efi-part) [ -n "$setup_part_efi" ] || setup_part_efi=$setup_av ;;
			swap-part) [ -n "$setup_part_swap" ] || setup_part_swap=$setup_av ;;
			home-part) [ -n "$setup_part_home" ] || setup_part_home=$setup_av ;;
			format) [ -n "$setup_format" ] || setup_format=$setup_av ;;
			kernel) [ -n "$setup_kernel" ] || setup_kernel=$setup_av ;;
			kernel-config) [ -n "$setup_kernel_config" ] || setup_kernel_config=$setup_av ;;
			drivers) [ -n "$setup_drivers" ] || setup_drivers=$setup_av ;;
			init) [ -n "$setup_init" ] || setup_init=$setup_av ;;
			services) [ -n "$setup_services" ] || setup_services=$setup_av ;;
			bootloader) [ "$setup_bootloader" = none ] && [ -n "$setup_av" ] && setup_bootloader=$setup_av ;;
			install-bootloader)
				[ "$setup_av" = yes ] && setup_install_bootloader=1
				[ "$setup_av" = no ] && setup_install_bootloader=0
				;;
			enable) setup_append_csv setup_enable "$setup_av" "$setup_enable" ;;
			disable) setup_append_csv setup_disable "$setup_av" "$setup_disable" ;;
			update)
				[ "$setup_av" = no ] && setup_do_update=0
				[ "$setup_av" = yes ] && setup_do_update=1
				;;
		esac
	done <"$setup_af"
}

[ -z "$setup_answers_in" ] || setup_load_answers "$setup_answers_in"

setup_yesno_norm()
{
	case $1 in
		yes|y|true|1) printf 'yes\n' ;;
		no|n|false|0|'') printf 'no\n' ;;
		*) setup_fail "$SPS_EX_USAGE" "expected yes or no, got '$1'" ;;
	esac
}

setup_dialog()
{
	command -v dialog >/dev/null 2>&1 ||
		setup_fail "$SPS_EX_PACKAGE" \
			"dialog is not on PATH; install it or use --non-interactive"
	dialog --backtitle "SPS Linux installer (Splux)" --output-fd 1 "$@"
}

setup_msg()
{
	if [ "$setup_interactive" -eq 1 ]; then
		dialog --backtitle "SPS Linux installer (Splux)" --msgbox "$1" 12 64 || :
	else
		printf '%s\n' "$1"
	fi
}

setup_confirm()
{
	if [ "$setup_interactive" -eq 1 ]; then
		dialog --backtitle "SPS Linux installer (Splux)" --yesno "$1" 14 64
	else
		return 0
	fi
}

setup_pick_disk()
{
	setup_dargs=
	setup_dline=
	setup_dname=
	setup_drest=
	if ! command -v setup_disk_menu_lines >/dev/null 2>&1; then
		setup_fail "$SPS_EX_PACKAGE" "partition helpers are missing"
	fi
	while IFS= read -r setup_dline || [ -n "$setup_dline" ]; do
		[ -n "$setup_dline" ] || continue
		setup_dname=${setup_dline%% *}
		setup_drest=$setup_dline
		# dialog tags cannot contain spaces; NAME is the tag.
		setup_dargs="$setup_dargs $setup_dname '$setup_drest'"
	done <<EOF
$(setup_disk_menu_lines)
EOF
	[ -n "$setup_dargs" ] ||
		setup_fail "$SPS_EX_NOTFOUND" "no disks found (is lsblk on PATH?)"
	eval "setup_dialog --title 'Select disk' \
		--menu 'Guided layouts erase this whole disk. Manage existing opens cfdisk first.' \
		18 72 10 $setup_dargs" ||
		setup_fail "$SPS_EX_USAGE" "cancelled"
}

setup_pick_partition()
{
	# $1 title, $2 allow none
	setup_title=$1
	setup_allow_none=$2
	setup_pargs=
	[ "$setup_allow_none" = 1 ] && setup_pargs="none 'No partition'"
	setup_pline=
	setup_pname=
	while IFS= read -r setup_pline || [ -n "$setup_pline" ]; do
		[ -n "$setup_pline" ] || continue
		setup_pname=${setup_pline%% *}
		setup_pargs="$setup_pargs $setup_pname '$setup_pline'"
	done <<EOF
$(setup_lsblk -n -p -o NAME,SIZE,TYPE,FSTYPE "$setup_disk" 2>/dev/null | awk '$3 == "part" { print }')
EOF
	eval "setup_dialog --title '$setup_title' \
		--menu 'Partition on $setup_disk' 18 72 10 $setup_pargs" ||
		setup_fail "$SPS_EX_USAGE" "cancelled"
}

setup_ask_partition()
{
	if [ -z "$setup_partition" ]; then
		setup_partition=$(setup_dialog --title "Disks" \
			--menu "Partitioning. Guided layouts wait until the final install confirmation before they write anything. Existing mounts are left alone." \
			18 72 8 \
			existing "Use mounts already on the target path" \
			manage "cfdisk, then pick root / EFI / swap / home" \
			efi-root "Guided: GPT, EFI + root" \
			efi-swap-root "Guided: GPT, EFI + swap + root" \
			efi-swap-root-home "Guided: GPT, EFI + swap + root + home" \
			bios-root "Guided: GPT, BIOS boot + root" \
			none "Do not touch disks") ||
			setup_fail "$SPS_EX_USAGE" "cancelled"
	fi

	case $setup_partition in
		none|existing)
			return 0
			;;
		manage|efi-root|efi-swap-root|efi-swap-root-home|bios-root)
			if [ -z "$setup_disk" ]; then
				setup_disk=$(setup_pick_disk) ||
					setup_fail "$SPS_EX_USAGE" "cancelled"
			fi
			if command -v setup_partition_tool_ok >/dev/null 2>&1 &&
			   ! setup_partition_tool_ok
			then
				setup_fail "$SPS_EX_PACKAGE" \
					"guided layouts need sfdisk, mkfs.ext4, mkfs.vfat, and mkswap on PATH"
			fi
			;;
	esac

	case $setup_partition in
		efi-root|efi-swap-root|efi-swap-root-home|bios-root)
			if [ -z "$setup_format" ]; then
				if setup_dialog --title "Format disk" --yesno \
					"Erase ALL data on $setup_disk and write a new $setup_partition partition table?

This is the format confirmation. Non-interactive installs pass --format yes. setup still waits until the last screen before it writes the disk." 14 64
				then
					setup_format=yes
				else
					setup_fail "$SPS_EX_USAGE" \
						"cancelled (guided layouts need a format confirmation)"
				fi
			fi
			;;
	esac

	if [ "$setup_partition" = manage ]; then
		if command -v cfdisk >/dev/null 2>&1 ||
		   [ -x /sbin/cfdisk ]; then
			setup_cfdisk=$(command -v cfdisk 2>/dev/null || printf /sbin/cfdisk)
			setup_msg "cfdisk will open on $setup_disk. Write the table, then quit. setup will ask which partitions to use."
			"$setup_cfdisk" "$setup_disk" || :
		else
			setup_msg "cfdisk is not on PATH. Using the partition table that is already on $setup_disk."
		fi
		setup_part_root=$(setup_pick_partition "Root partition" 0)
		[ "$setup_part_root" != none ] ||
			setup_fail "$SPS_EX_USAGE" "root partition is required"
		setup_part_efi=$(setup_pick_partition "EFI partition" 1)
		[ "$setup_part_efi" != none ] || setup_part_efi=
		setup_part_swap=$(setup_pick_partition "Swap partition" 1)
		[ "$setup_part_swap" != none ] || setup_part_swap=
		setup_part_home=$(setup_pick_partition "Home partition" 1)
		[ "$setup_part_home" != none ] || setup_part_home=
		if setup_dialog --title "Format" --yesno \
			"Format the selected partitions (ext4 / vfat / swap) before mounting them on the target?

This erases data on those partitions only, not necessarily the whole disk." 12 64
		then
			setup_format=yes
		else
			setup_format=no
		fi
	fi
}

setup_ask_kernel()
{
	if [ -n "$setup_kernel" ]; then
		return 0
	fi
	setup_kdef=none
	case $setup_profile in
		plasma-desktop|plasma-full) setup_kdef=linux-desktop ;;
	esac
	setup_kernel=$(setup_dialog --title "SPS Linux kernel" --default-item "$setup_kdef" \
		--menu "none leaves the host kernel story alone (current live ISO). linux is upstream defconfig. linux-desktop is the SPS Linux (Splux) daily-driver config: DRM (nouveau/i915/amdgpu), NVMe, audio, extra filesystems. custom copies a .config you already have." \
		18 72 6 \
		none "Do not build a kernel package" \
		linux-desktop "SPS Linux desktop config (Splux; Plasma default)" \
		linux "Upstream defconfig" \
		custom "Use a .config from this machine") ||
		setup_fail "$SPS_EX_USAGE" "cancelled"
	if [ "$setup_kernel" = custom ] && [ -z "$setup_kernel_config" ]; then
		setup_kernel_config=$(setup_dialog --title "Kernel config" \
			--inputbox "Absolute path to a Linux .config. It is copied to the target as /etc/sps/kernel.config before the kernel package builds." \
			10 72 "${setup_kernel_config:-/etc/sps/kernel.config}") ||
			setup_fail "$SPS_EX_USAGE" "cancelled"
	fi
	# If the user just hit enter on the default-looking first item, keep the
	# profile default when they picked none on a Plasma install? No: none is
	# an explicit choice. Profile default only applies when the flag is omitted
	# in non-interactive mode.
	:
}

setup_ask_init()
{
	if [ -n "$setup_init" ]; then
		return 0
	fi
	setup_init_default=systemd
	if [ -f /etc/sps/init ]; then
		setup_live_init=$(sed -n '1p' /etc/sps/init)
		case $setup_live_init in
			systemd|openrc|s6|runit|dinit|shepherd|sysvinit)
				setup_init_default=$setup_live_init
				;;
		esac
	fi
	setup_init=$(setup_dialog --title "Init system" --default-item "$setup_init_default" \
		--menu "PID 1 on the installed system. These packages conflict; sget will refuse more than one. Non-systemd inits pull eudev and elogind. Daemon packages ship scripts for every init into the right directories; setup enables the detected init." \
		22 76 9 \
		systemd "systemd (udev, logind, the default)" \
		openrc "OpenRC plus eudev and elogind" \
		s6 "s6 plus eudev and elogind" \
		runit "runit plus eudev and elogind" \
		dinit "dinit plus eudev and elogind" \
		shepherd "GNU Shepherd plus eudev, elogind, and Guile" \
		sysvinit "SysV init plus eudev and elogind") ||
		setup_fail "$SPS_EX_USAGE" "cancelled"
}

setup_ask_drivers()
{
	if [ -n "$setup_drivers" ]; then
		return 0
	fi
	setup_hwtxt="Could not probe PCI."
	if command -v setup_hw_summary >/dev/null 2>&1; then
		setup_hwtxt=$(setup_hw_summary)
	fi
	if setup_dialog --title "Drivers and microcode" --yesno \
		"Detect hardware and install matching packages from the repos?

$setup_hwtxt

This pulls CPU microcode, Mesa/nouveau, firmware, and wireless helpers. Proprietary NVIDIA modules stay off unless you enable them on the next screen." 18 72
	then
		setup_drivers=auto
	else
		setup_drivers=none
	fi
}


# Interactive defaults when flags were omitted.
if [ "$setup_interactive" -eq 1 ] && [ ! -t 0 ]; then
	setup_fail "$SPS_EX_USAGE" \
		"stdin is not a terminal; use --non-interactive"
fi

if [ "$setup_interactive" -eq 1 ]; then
	command -v dialog >/dev/null 2>&1 ||
		setup_fail "$SPS_EX_PACKAGE" \
			"dialog is required for interactive setup"
fi

setup_work=$(sps_mktemp_dir "${TMPDIR:-/tmp}" setup) || exit $?
trap setup_cleanup 0 HUP INT TERM
setup_collect_recipe_names "$setup_work/recipe-names"

if [ "$setup_interactive" -eq 1 ]; then
	if [ -f /etc/sps/live ]; then
		setup_msg \
			"This is the SPS Linux live image (Splux). You can partition a disk here, or mount something on /mnt first and choose Use existing mounts.

Guided layouts wait until the last confirmation before they erase a disk. Bootloader install is a separate, explicit step (needs --install-bootloader and --disk)."
	else
		setup_msg \
			"This installer configures SPS Linux (Splux) the way Slackware and FreeBSD setup do: disks, profile, kernel, drivers, then a visible package install.

Guided partition layouts erase the chosen disk only after you confirm at the end. grub-install is never implied. An LFS tree using SPS does not have to go through this installer."
	fi

	setup_ask_partition

	if [ -z "$setup_target" ]; then
		setup_target=$(setup_dialog --title "Installation target" \
			--inputbox "Absolute path of the target root.

Use /mnt after mounting the destination filesystem, or after a guided layout (setup mounts it for you at the end). / is the live system." \
			10 64 "${setup_target:-/mnt}") ||
			setup_fail "$SPS_EX_USAGE" "cancelled"
	fi

	if [ -z "$setup_profile" ]; then
		setup_menu_args=
		for setup_pf in "$setup_dir"/profiles/*.profile; do
			[ -f "$setup_pf" ] || continue
			setup_pn=$(setup_field "$setup_pf" name)
			setup_pt=$(setup_field "$setup_pf" title)
			setup_menu_args="$setup_menu_args $setup_pn '$setup_pt'"
		done
		setup_profile=$(eval "setup_dialog --title 'Installation profile' \
			--menu 'Choose a base system.' 16 64 8 $setup_menu_args") ||
			setup_fail "$SPS_EX_USAGE" "cancelled"
	fi

	setup_ask_kernel
	setup_ask_init

	if [ -z "$setup_hostname" ]; then
		setup_hostname=$(setup_dialog --title "Hostname" \
			--inputbox "System hostname." 8 50 "${setup_hostname:-splux}") ||
			setup_fail "$SPS_EX_USAGE" "cancelled"
	fi

	if [ -z "$setup_timezone" ]; then
		setup_tz_args=
		while IFS= read -r setup_tz || [ -n "$setup_tz" ]; do
			[ -n "$setup_tz" ] || continue
			setup_tz_args="$setup_tz_args $setup_tz '$setup_tz'"
		done <"$setup_dir/timezones"
		setup_timezone=$(eval "setup_dialog --title 'Timezone' \
			--menu 'Select a timezone.' 20 60 12 $setup_tz_args") ||
			setup_fail "$SPS_EX_USAGE" "cancelled"
	fi

	if [ -z "$setup_locale" ]; then
		setup_loc_args=
		while IFS= read -r setup_loc || [ -n "$setup_loc" ]; do
			[ -n "$setup_loc" ] || continue
			setup_loc_args="$setup_loc_args $setup_loc '$setup_loc'"
		done <"$setup_dir/locales"
		setup_locale=$(eval "setup_dialog --title 'Locale' \
			--menu 'LANG for /etc/locale.conf. C.UTF-8 needs no generated locales.' 20 60 12 $setup_loc_args") ||
			setup_fail "$SPS_EX_USAGE" "cancelled"
	fi

	if [ -z "$setup_keymap" ]; then
		setup_km_args=
		while IFS= read -r setup_km || [ -n "$setup_km" ]; do
			[ -n "$setup_km" ] || continue
			setup_km_args="$setup_km_args $setup_km '$setup_km'"
		done <"$setup_dir/keymaps"
		setup_keymap=$(eval "setup_dialog --title 'Keyboard' \
			--menu 'Keyboard map for the console.' 18 50 12 $setup_km_args") ||
			setup_fail "$SPS_EX_USAGE" "cancelled"
	fi

	setup_chk_args=
	for setup_sf in "$setup_dir"/sets/*.set; do
		[ -f "$setup_sf" ] || continue
		setup_sn=$(setup_field "$setup_sf" name)
		setup_st=$(setup_field "$setup_sf" title)
		setup_sd=$(setup_field "$setup_sf" description)
		setup_on=off
		case " $setup_enable " in *" $setup_sn "*) setup_on=on ;; esac
		if [ -z "$setup_enable" ]; then
			setup_def=$(setup_field "$setup_sf" default)
			if [ "$setup_def" = 1 ]; then
				case $setup_profile in
					plasma-desktop|plasma-full) setup_on=on ;;
					*)
						[ "$setup_sn" = qol-cli ] && setup_on=on
						[ "$setup_sn" = power ] && setup_on=off
						[ "$setup_sn" = firmware ] && setup_on=off
						;;
				esac
			fi
		fi
		case " $setup_disable " in *" $setup_sn "*) setup_on=off ;; esac
		setup_chk_args="$setup_chk_args $setup_sn '$setup_st' $setup_on"
	done
	setup_sel=$(eval "setup_dialog --title 'Optional sets' \
		--checklist 'Space toggles a set. These are installed in addition to the profile.' \
		20 72 12 $setup_chk_args") ||
		setup_fail "$SPS_EX_USAGE" "cancelled"
	setup_enable=
	# dialog may quote tags
	set -- $setup_sel
	for setup_tok; do
		setup_tok=${setup_tok#\"}
		setup_tok=${setup_tok%\"}
		[ -n "$setup_tok" ] || continue
		setup_append_csv setup_enable "$setup_tok" "$setup_enable"
	done

	if [ -f "$setup_dir/extras" ]; then
		setup_ex_args=
		while IFS= read -r setup_exline || [ -n "$setup_exline" ]; do
			[ -n "$setup_exline" ] || continue
			case $setup_exline in \#*) continue ;; esac
			setup_en=${setup_exline%%	*}
			setup_et=${setup_exline#*	}
			[ -n "$setup_en" ] || continue
			[ "$setup_et" != "$setup_exline" ] || setup_et=$setup_en
			# Live recipe trees are the source of truth. Unpackaged
			# names (mpv, LibreOffice, ...) must not reach sget.
			if [ -s "$setup_work/recipe-names" ] &&
				! grep -qx "$setup_en" "$setup_work/recipe-names"
			then
				printf '%s: skipping unpackaged extra %s\n' \
					"$sps_prog" "$setup_en" >&2
				continue
			fi
			setup_on=off
			case " $setup_extra " in *" $setup_en "*) setup_on=on ;; esac
			setup_ex_args="$setup_ex_args $setup_en '$setup_et' $setup_on"
		done <"$setup_dir/extras"
		if [ -n "$setup_ex_args" ]; then
			setup_sel=$(eval "setup_dialog --title 'Extra packages' \
				--checklist 'Optional packages on top of the profile and sets. Space toggles an item.' \
				22 72 14 $setup_ex_args") ||
				setup_fail "$SPS_EX_USAGE" "cancelled"
			set -- $setup_sel
			for setup_tok; do
				setup_tok=${setup_tok#\"}
				setup_tok=${setup_tok%\"}
				[ -n "$setup_tok" ] || continue
				setup_append_csv setup_extra "$setup_tok" "$setup_extra"
			done
		fi
	fi

	setup_ask_drivers

	if [ -z "$setup_nvidia" ]; then
		if setup_dialog --title "NVIDIA proprietary" --yesno \
			"Include NVIDIA open kernel modules and user-space utilities?

The live image and auto drivers use nouveau. This extra set is the proprietary/open-rm stack and needs a matching kernel on the target. Off by default." 12 64
		then
			setup_nvidia=yes
		else
			setup_nvidia=no
		fi
	fi

	if [ -z "$setup_flatpak" ]; then
		if setup_dialog --title "Flatpak" --yesno \
			"Install Flatpak and add the Flathub remote after package installation?" 9 64
		then
			setup_flatpak=yes
		else
			setup_flatpak=no
		fi
	fi

	if [ -z "$setup_user" ]; then
		setup_user=$(setup_dialog --title "First user" \
			--inputbox "Unprivileged account (uid 1000). Leave empty to skip." \
			9 50 "${setup_user:-}") ||
			setup_fail "$SPS_EX_USAGE" "cancelled"
	fi

	if [ -z "$setup_user_gecos" ] && [ -n "$setup_user" ]; then
		setup_user_gecos=$(setup_dialog --title "User full name" \
			--inputbox "GECOS / display name for $setup_user." \
			8 50 "$setup_user") ||
			setup_fail "$SPS_EX_USAGE" "cancelled"
	fi

	if [ -n "$setup_user" ] && [ -z "$setup_user_password_hash" ]; then
		setup_user_password_hash=$(setup_ask_password_pair "User password" \
			"Password for $setup_user. Leave empty to lock the account until you run passwd after boot.")
	fi
	if [ -z "$setup_root_password_hash" ]; then
		setup_root_password_hash=$(setup_ask_password_pair "Root password" \
			"Password for root. Leave empty to keep root locked.")
	fi

	if [ -z "$setup_shell" ]; then
		setup_sh_args=
		if [ -f "$setup_dir/shells" ]; then
			while IFS= read -r setup_sh || [ -n "$setup_sh" ]; do
				[ -n "$setup_sh" ] || continue
				setup_sh_args="$setup_sh_args $setup_sh '$setup_sh'"
			done <"$setup_dir/shells"
		fi
		[ -n "$setup_sh_args" ] || setup_sh_args="/bin/sh '/bin/sh' /bin/bash '/bin/bash'"
		setup_shell=$(eval "setup_dialog --title 'Login shell' \
			--menu 'Login shell for the first user, or the default account later. zsh and fish need the shells-extra set.' 16 64 6 $setup_sh_args") ||
			setup_fail "$SPS_EX_USAGE" "cancelled"
	fi

	setup_dialog --title "Network" --msgbox \
		"This installer does not configure DHCP, static addresses, or wireless.

After the first boot, put an interface file in /etc/network or use the NetworkManager applet on Plasma. Notes are written to /root/sps-network.txt." 12 64 || :

	setup_bl=$(setup_dialog --title "Bootloader" \
		--menu "SPS never guesses a grub-install target. Notes are always written to /root/sps-bootloader.txt. Install GRUB only if you pick the last item and confirm the disk." \
		16 72 4 \
		none "Write /root/sps-bootloader.txt only" \
		grub "Write GRUB notes (still does not install)" \
		install "Install GRUB to the chosen disk (asks again)") ||
		setup_fail "$SPS_EX_USAGE" "cancelled"
	case $setup_bl in
		install)
			setup_bootloader=grub
			if [ -z "$setup_disk" ]; then
				setup_msg "No disk was selected. Writing GRUB notes only. Pass a disk on the partition screen, or run grub-install yourself later."
			elif setup_dialog --title "Install GRUB" --yesno \
				"Run grub-install on $setup_disk now?

This is the only interactive path that installs a bootloader. Scripts still need --install-bootloader and --disk together." 12 64
			then
				PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH
				export PATH
				if command -v grub-install >/dev/null 2>&1; then
					setup_install_bootloader=1
				else
					setup_msg "grub-install is not on PATH. Notes will be written; install GRUB after the packages land or from another system."
				fi
			fi
			;;
		*)
			setup_bootloader=$setup_bl
			;;
	esac
fi

# Non-interactive defaults
[ -n "$setup_target" ] || setup_target=/mnt
[ -n "$setup_profile" ] || setup_profile=minimal
[ -n "$setup_hostname" ] || setup_hostname=splux
[ -n "$setup_timezone" ] || setup_timezone=UTC
[ -n "$setup_keymap" ] || setup_keymap=us
[ -n "$setup_locale" ] || setup_locale=C.UTF-8
[ -n "$setup_shell" ] || setup_shell=/bin/sh
setup_flatpak=$(setup_yesno_norm "${setup_flatpak:-no}")
setup_nvidia=$(setup_yesno_norm "${setup_nvidia:-no}")
setup_ssh=$(setup_yesno_norm "${setup_ssh:-no}")
setup_printing=$(setup_yesno_norm "${setup_printing:-no}")
[ -n "$setup_partition" ] || setup_partition=none
[ -n "$setup_drivers" ] || setup_drivers=none
[ -n "$setup_init" ] || setup_init=none
[ -n "$setup_format" ] || setup_format=no
setup_format=$(setup_yesno_norm "$setup_format")
setup_services=$(setup_yesno_norm "${setup_services:-yes}")
if [ -z "$setup_kernel" ]; then
	setup_kernel=none
fi
case $setup_kernel in
	none|linux|linux-desktop|custom) ;;
	*) setup_fail "$SPS_EX_USAGE" "kernel must be none, linux, linux-desktop, or custom" ;;
esac
case $setup_drivers in
	auto|none) ;;
	*) setup_fail "$SPS_EX_USAGE" "drivers must be auto or none" ;;
esac
case $setup_init in
	none|systemd|openrc|s6|runit|dinit|shepherd|sysvinit) ;;
	*) setup_fail "$SPS_EX_USAGE" "init must be systemd, openrc, s6, runit, dinit, shepherd, sysvinit, or none" ;;
esac
case $setup_services in
	yes|no) ;;
	*) setup_fail "$SPS_EX_USAGE" "services must be yes or no" ;;
esac
case $setup_partition in
	none|existing|efi-root|efi-swap-root|efi-swap-root-home|bios-root|manage) ;;
	*) setup_fail "$SPS_EX_USAGE" "unknown partition layout: $setup_partition" ;;
esac
case $setup_partition in
	efi-root|efi-swap-root|efi-swap-root-home|bios-root)
		[ -n "$setup_disk" ] ||
			setup_fail "$SPS_EX_USAGE" "guided --partition needs --disk"
		case $setup_disk in
			/dev/*) ;;
			*) setup_fail "$SPS_EX_USAGE" "disk must be a /dev path" ;;
		esac
		if [ "$setup_plan" -eq 0 ] && [ "$setup_format" != yes ]; then
			setup_fail "$SPS_EX_USAGE" \
				"guided partitioning requires --format yes (refusing to erase $setup_disk)"
		fi
		if [ "$setup_plan" -eq 0 ]; then
			if command -v setup_partition_tool_ok >/dev/null 2>&1 &&
			   ! setup_partition_tool_ok
			then
				setup_fail "$SPS_EX_PACKAGE" \
					"guided layouts need sfdisk, mkfs.ext4, mkfs.vfat, and mkswap on PATH"
			fi
		fi
		;;
	manage)
		[ -n "$setup_part_root" ] ||
			setup_fail "$SPS_EX_USAGE" "--partition manage needs --root-part"
		;;
esac
if [ "$setup_kernel" = custom ]; then
	[ -n "$setup_kernel_config" ] ||
		setup_fail "$SPS_EX_USAGE" "--kernel custom requires --kernel-config FILE"
	case $setup_kernel_config in
		/*) ;;
		*) setup_fail "$SPS_EX_USAGE" "kernel config must be an absolute path" ;;
	esac
	[ -f "$setup_kernel_config" ] ||
		setup_fail "$SPS_EX_NOTFOUND" "kernel config not found: $setup_kernel_config"
fi
if [ "$setup_install_bootloader" -eq 1 ]; then
	[ -n "$setup_disk" ] ||
		setup_fail "$SPS_EX_USAGE" \
			"--install-bootloader requires --disk; refusing to guess"
	PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH
	export PATH
	if [ "$setup_plan" -eq 0 ] && ! command -v grub-install >/dev/null 2>&1; then
		setup_fail "$SPS_EX_PACKAGE" "grub-install is not on PATH"
	fi
fi
case $setup_bootloader in
	none|grub) ;;
	*) setup_fail "$SPS_EX_USAGE" "bootloader must be none or grub" ;;
esac

setup_valid_profile "$setup_profile" ||
	setup_fail "$SPS_EX_NOTFOUND" "unknown profile: $setup_profile"

case $setup_target in
	/*) ;;
	*) setup_fail "$SPS_EX_USAGE" "target must be an absolute path: $setup_target" ;;
esac

case $setup_hostname in
	*[!A-Za-z0-9.-]*|'') setup_fail "$SPS_EX_USAGE" "invalid hostname: $setup_hostname" ;;
esac

case $setup_locale in
	*[!A-Za-z0-9._+-]*|'') setup_fail "$SPS_EX_USAGE" "invalid locale: $setup_locale" ;;
esac

case $setup_shell in
	/*) ;;
	*) setup_fail "$SPS_EX_USAGE" "shell must be an absolute path: $setup_shell" ;;
esac

if [ -n "$setup_user" ]; then
	case $setup_user in
		*[!a-z0-9_-]*|''|root|nobody)
			setup_fail "$SPS_EX_USAGE" "invalid user name: $setup_user"
			;;
	esac
fi

# Non-interactive installs start from profile defaults, then --enable.
if [ "$setup_interactive" -eq 0 ]; then
	setup_defs=$(setup_default_sets_for_profile)
	for setup_def in $setup_defs; do
		setup_append_csv setup_enable "$setup_def" "$setup_enable"
	done
fi

# Apply nvidia/flatpak convenience flags to sets.
if [ "$setup_nvidia" = yes ]; then
	setup_append_csv setup_enable nvidia "$setup_enable"
fi
if [ "$setup_flatpak" = yes ]; then
	setup_append_csv setup_enable flatpak "$setup_enable"
fi
if [ "$setup_ssh" = yes ]; then
	setup_append_csv setup_enable ssh "$setup_enable"
fi
if [ "$setup_printing" = yes ]; then
	setup_append_csv setup_enable printing "$setup_enable"
fi
case $setup_shell in
	*/zsh|*/fish)
		setup_append_csv setup_enable shells-extra "$setup_enable"
		;;
esac

# --disable wins
setup_en_new=
for setup_item in $setup_enable; do
	setup_skip=0
	for setup_d in $setup_disable; do
		[ "$setup_item" = "$setup_d" ] && setup_skip=1
	done
	[ "$setup_skip" -eq 0 ] || continue
	setup_valid_set "$setup_item" ||
		setup_fail "$SPS_EX_NOTFOUND" "unknown set: $setup_item"
	setup_en_new="$setup_en_new $setup_item"
done
setup_enable=$(printf '%s' "$setup_en_new" | sed 's/^ //')

setup_pkg_file=$setup_work/packages
: >"$setup_pkg_file"
setup_add_pkg()
{
	setup_pkg=$1
	[ -n "$setup_pkg" ] || return 0
	grep -qx "$setup_pkg" "$setup_pkg_file" 2>/dev/null && return 0
	printf '%s\n' "$setup_pkg" >>"$setup_pkg_file"
}

setup_packages_from "$(setup_profile_file "$setup_profile")" |
	while IFS= read -r setup_pkg; do
		setup_add_pkg "$setup_pkg"
	done

# The pipeline subshell cannot update the file... actually it can, the file is
# in the filesystem. setup_add_pkg appends. Good.

for setup_item in $setup_enable; do
	setup_packages_from "$(setup_set_file "$setup_item")" |
		while IFS= read -r setup_pkg; do
			setup_add_pkg "$setup_pkg"
		done
done

for setup_item in $setup_extra; do
	setup_add_pkg "$setup_item"
done

# Chosen init wins over set/package combinations that sget would refuse.
for setup_item in $setup_extra $setup_enable; do
	[ -n "$setup_item" ] || continue
	if setup_pkg_conflicts_init "$setup_init" "$setup_item"; then
		setup_fail "$SPS_EX_CONFLICT" \
			"cannot combine --init $setup_init with $setup_item"
	fi
done

setup_pkg_filtered=$setup_work/packages.filtered
: >"$setup_pkg_filtered"
while IFS= read -r setup_pkg || [ -n "$setup_pkg" ]; do
	[ -n "$setup_pkg" ] || continue
	if setup_pkg_conflicts_init "$setup_init" "$setup_pkg"; then
		printf '%s: dropping %s (conflicts with init %s)\n' \
			"$sps_prog" "$setup_pkg" "$setup_init" >&2
		continue
	fi
	printf '%s\n' "$setup_pkg" >>"$setup_pkg_filtered"
done <"$setup_pkg_file"
mv "$setup_pkg_filtered" "$setup_pkg_file"

case $setup_kernel in
	linux) setup_add_pkg linux ;;
	linux-desktop) setup_add_pkg linux-desktop ;;
	custom) setup_add_pkg linux-custom ;;
esac

case $setup_init in
	systemd) setup_add_pkg init-systemd ;;
	openrc) setup_add_pkg init-openrc ;;
	s6) setup_add_pkg init-s6 ;;
	runit) setup_add_pkg init-runit ;;
	dinit) setup_add_pkg init-dinit ;;
	shepherd) setup_add_pkg init-shepherd ;;
	sysvinit) setup_add_pkg init-sysvinit ;;
esac

if [ "$setup_drivers" = auto ]; then
	if command -v setup_hw_packages >/dev/null 2>&1; then
		setup_hw_packages | sort -u |
			while IFS= read -r setup_pkg; do
				[ -n "$setup_pkg" ] || continue
				setup_add_pkg "$setup_pkg"
			done
	fi
fi

setup_write_answers_file()
{
	setup_out=$1
	{
		printf 'target %s\n' "$setup_target"
		printf 'profile %s\n' "$setup_profile"
		printf 'hostname %s\n' "$setup_hostname"
		printf 'timezone %s\n' "$setup_timezone"
		printf 'keymap %s\n' "$setup_keymap"
		printf 'locale %s\n' "$setup_locale"
		printf 'shell %s\n' "$setup_shell"
		[ -n "$setup_user" ] && printf 'user %s\n' "$setup_user"
		[ -n "$setup_user_gecos" ] && printf 'user-gecos %s\n' "$setup_user_gecos"
		printf 'flatpak %s\n' "$setup_flatpak"
		printf 'nvidia %s\n' "$setup_nvidia"
		printf 'ssh %s\n' "$setup_ssh"
		printf 'printing %s\n' "$setup_printing"
		printf 'bootloader %s\n' "$setup_bootloader"
		printf 'partition %s\n' "$setup_partition"
		[ -n "$setup_disk" ] && printf 'disk %s\n' "$setup_disk"
		printf 'swap-size %s\n' "$setup_swap_size"
		[ -n "$setup_part_root" ] && printf 'root-part %s\n' "$setup_part_root"
		[ -n "$setup_part_efi" ] && printf 'efi-part %s\n' "$setup_part_efi"
		[ -n "$setup_part_swap" ] && printf 'swap-part %s\n' "$setup_part_swap"
		[ -n "$setup_part_home" ] && printf 'home-part %s\n' "$setup_part_home"
		printf 'format %s\n' "$setup_format"
		printf 'kernel %s\n' "$setup_kernel"
		[ -n "$setup_kernel_config" ] && printf 'kernel-config %s\n' "$setup_kernel_config"
		printf 'drivers %s\n' "$setup_drivers"
		printf 'init %s\n' "$setup_init"
		printf 'services %s\n' "$setup_services"
		printf 'install-bootloader %s\n' \
			"$([ "$setup_install_bootloader" -eq 1 ] && printf yes || printf no)"
		printf 'update %s\n' "$([ "$setup_do_update" -eq 1 ] && printf yes || printf no)"
		for setup_item in $setup_enable; do
			printf 'enable %s\n' "$setup_item"
		done
		for setup_item in $setup_extra; do
			printf 'extra %s\n' "$setup_item"
		done
	} >"$setup_out"
}

setup_summary()
{
	printf 'Target:     %s\n' "$setup_target"
	printf 'Profile:    %s\n' "$setup_profile"
	printf 'Hostname:   %s\n' "$setup_hostname"
	printf 'Timezone:   %s\n' "$setup_timezone"
	printf 'Keymap:     %s\n' "$setup_keymap"
	printf 'Locale:     %s\n' "$setup_locale"
	printf 'Shell:      %s\n' "$setup_shell"
	printf 'User:       %s\n' "${setup_user:-<none>}"
	if [ -n "$setup_user_password_hash" ]; then
		printf 'User pw:    set\n'
	elif [ -n "$setup_user" ]; then
		printf 'User pw:    locked\n'
	fi
	if [ -n "$setup_root_password_hash" ]; then
		printf 'Root pw:    set\n'
	else
		printf 'Root pw:    locked\n'
	fi
	printf 'Sets:       %s\n' "${setup_enable:-<none>}"
	printf 'Extra:      %s\n' "${setup_extra:-<none>}"
	printf 'Flatpak:    %s\n' "$setup_flatpak"
	printf 'NVIDIA:     %s\n' "$setup_nvidia"
	printf 'SSH:        %s\n' "$setup_ssh"
	printf 'Printing:   %s\n' "$setup_printing"
	printf 'Bootloader: %s\n' "$setup_bootloader"
	printf 'Partition:  %s\n' "$setup_partition"
	printf 'Disk:       %s\n' "${setup_disk:-<none>}"
	printf 'Kernel:     %s\n' "$setup_kernel"
	printf 'Init:       %s\n' "$setup_init"
	printf 'Drivers:    %s\n' "$setup_drivers"
	printf 'Services:   %s\n' "$setup_services"
	printf 'Format:     %s\n' "$setup_format"
	if [ "$setup_install_bootloader" -eq 1 ]; then
		printf 'GRUB:       install on %s\n' "$setup_disk"
	fi
	if [ "$setup_init" = none ] &&
	   grep -Eq '^(sddm|plasma-desktop|plasma-full)$' "$setup_pkg_file"
	then
		printf '\nNote: init is none; SDDM/Plasma will not start at boot. Choose systemd, OpenRC, s6, runit, dinit, Shepherd, or SysV.\n'
	fi
	if [ "$setup_kernel" = none ]; then
		case $setup_partition in
			efi-root|efi-swap-root|efi-swap-root-home|bios-root)
				printf '\nNote: no kernel package was selected; the installed disk will not boot until you add one.\n'
				;;
		esac
	fi
	printf '\nPackages:\n'
	sort -u "$setup_pkg_file"
}

if [ -n "$setup_answers_out" ]; then
	setup_write_answers_file "$setup_answers_out"
fi

if [ "$setup_plan" -eq 1 ]; then
	setup_summary
	exit 0
fi

if [ "$setup_interactive" -eq 1 ]; then
	setup_warn=
	case $setup_partition in
		efi-root|efi-swap-root|efi-swap-root-home|bios-root)
			setup_warn="
THIS ERASES $setup_disk. Partition table, filesystems, all of it."
			;;
		manage)
			if [ "$setup_format" = yes ]; then
				setup_warn="
Selected partitions will be formatted: $setup_part_root $setup_part_efi $setup_part_swap $setup_part_home"
			fi
			;;
	esac
	setup_confirm "Install with this configuration?

$(setup_summary)
$setup_warn

Package builds run on this terminal. Source packages may take a long time." ||
		setup_fail "$SPS_EX_USAGE" "cancelled"
fi

# Guided layouts run here, after the last chance to cancel.
case $setup_partition in
	none|existing) ;;
	*)
		if [ "$setup_plan" -eq 0 ]; then
			printf '%s: applying partition layout %s on %s\n' \
				"$sps_prog" "$setup_partition" "${setup_disk:-partitions}" >&2
			PATH=/sbin:/usr/sbin:$PATH
			export PATH
			setup_apply_partition "$setup_target" ||
				setup_fail "$SPS_EX_PERMISSION" "partitioning failed"
		fi
		;;
esac

setup_apply_config()
{
	setup_etc=$setup_root/etc
	mkdir -p "$setup_etc/sps" "$setup_root/root" \
		"$setup_etc" ||
		setup_fail "$SPS_EX_PERMISSION" "cannot create target directories"

	printf '%s\n' "$setup_hostname" >"$setup_etc/hostname"
	printf '%s\n' "$setup_timezone" >"$setup_etc/timezone"
	printf 'KEYMAP=%s\n' "$setup_keymap" >"$setup_etc/vconsole.conf"
	printf 'keymap %s\n' "$setup_keymap" >"$setup_etc/sps/keymap"
	printf 'LANG=%s\n' "$setup_locale" >"$setup_etc/locale.conf"
	printf '%s\n' "${setup_init:-none}" >"$setup_etc/sps/init"

	if [ -f /usr/share/zoneinfo/"$setup_timezone" ]; then
		cp /usr/share/zoneinfo/"$setup_timezone" "$setup_etc/localtime" 2>/dev/null || :
	fi

	setup_write_answers_file "$setup_etc/sps/setup-answers"
	# Installed-system paths. src update during setup used the live
	# system's bundled trees; the installed machine tracks the official
	# Git remotes so it keeps receiving recipe updates.
	setup_write_repos_conf ""

	if [ ! -f "$setup_etc/passwd" ]; then
		printf '%s\n' \
			'root:x:0:0:root:/root:/bin/sh' \
			'nobody:x:65534:65534:Unprivileged User:/:/usr/bin/nologin' \
			>"$setup_etc/passwd"
		printf '%s\n' 'root:x:0:' 'wheel:x:10:' 'audio:x:17:' \
			'video:x:18:' 'nogroup:x:65534:' \
			>"$setup_etc/group"
		printf '%s\n' \
			'root:!:0:0:99999:7:::' \
			'nobody:!:0:0:99999:7:::' \
			>"$setup_etc/shadow"
	fi
	setup_group_ensure "$setup_etc/group" wheel 10
	setup_group_ensure "$setup_etc/group" audio 17
	setup_group_ensure "$setup_etc/group" video 18
	setup_group_ensure "$setup_etc/group" input 97
	setup_group_ensure "$setup_etc/group" kvm 36
	setup_group_ensure "$setup_etc/group" users 100
	if [ -f "$setup_etc/shadow" ] && ! grep -q '^root:' "$setup_etc/shadow"
	then
		printf '%s\n' 'root:!:0:0:99999:7:::' >>"$setup_etc/shadow"
	fi

	if [ -n "$setup_user" ]; then
		setup_home=/home/$setup_user
		mkdir -p "$setup_root$setup_home"
		if ! grep -q "^$setup_user:" "$setup_etc/passwd"; then
			printf '%s:x:1000:1000:%s:%s:%s\n' \
				"$setup_user" \
				"${setup_user_gecos:-$setup_user}" \
				"$setup_home" \
				"$setup_shell" >>"$setup_etc/passwd"
		fi
		if [ -f "$setup_etc/group" ] &&
		   ! grep -q "^$setup_user:" "$setup_etc/group"; then
			printf '%s:x:1000:\n' "$setup_user" >>"$setup_etc/group"
		fi
		if [ -f "$setup_etc/shadow" ] &&
		   ! grep -q "^$setup_user:" "$setup_etc/shadow"; then
			printf '%s:!:0:0:99999:7:::\n' "$setup_user" >>"$setup_etc/shadow"
		fi
		for setup_g in wheel audio video input kvm users
		do
			setup_group_add_user "$setup_etc/group" "$setup_g" "$setup_user"
		done
		chown -R 1000:1000 "$setup_root$setup_home" 2>/dev/null || :
		chmod 0750 "$setup_root$setup_home" 2>/dev/null || :
		setup_shadow_set_hash "$setup_etc/shadow" "$setup_user" \
			"$setup_user_password_hash"
		if [ -n "$setup_user_password_hash" ]; then
			cat >"$setup_root/root/sps-user.txt" <<EOF
The account '$setup_user' (uid 1000) was recorded in /etc/passwd.
The password was set during setup. The account is in wheel, audio, video,
input, kvm, and users when those groups exist.
EOF
		else
			cat >"$setup_root/root/sps-user.txt" <<EOF
The account '$setup_user' (uid 1000) was recorded in /etc/passwd.
The password is locked. From a live root run:

    passwd $setup_user
EOF
		fi
	fi
	setup_shadow_set_hash "$setup_etc/shadow" root "$setup_root_password_hash"
	if [ -n "$setup_root_password_hash" ]; then
		mkdir -p "$setup_root/root"
		if [ -f "$setup_root/root/sps-user.txt" ]; then
			printf '%s\n' 'The root password was set during setup.' \
				>>"$setup_root/root/sps-user.txt"
		else
			printf '%s\n' 'The root password was set during setup.' \
				>"$setup_root/root/sps-user.txt"
		fi
	elif [ ! -f "$setup_root/root/sps-user.txt" ]; then
		mkdir -p "$setup_root/root"
		printf '%s\n' 'The root password is locked. Run passwd after boot.' \
			>"$setup_root/root/sps-user.txt"
	fi

	cat >"$setup_root/root/sps-bootloader.txt" <<EOF
SPS does not run grub-install or change EFI variables during setup.

To install GRUB yourself, after chrooting into $setup_root:

    grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=SPS
    grub-mkconfig -o /boot/grub/grub.cfg

Or for BIOS:

    grub-install /dev/DISK
    grub-mkconfig -o /boot/grub/grub.cfg

Replace DISK with the actual disk. This is an administrator action.
EOF

	cat >"$setup_root/root/sps-init.txt" <<EOF
Init choice: ${setup_init:-none}

These PID 1 packages conflict; sget will not install more than one.
After the packages land:

  systemd   PID 1 is /usr/lib/systemd/systemd. Enable getty@tty1
            and, for Plasma, a display manager. /etc/machine-id is
            created on first boot if missing.
  openrc    PID 1 is OpenRC. Add services with rc-update. eudev and
            elogind are in the init-openrc set.
  s6        PID 1 execs s6-svscan on /run/service. Enabled longruns
            live in /etc/s6/enabled. eudev and elogind come with
            init-s6. s6-linux-init-maker is installed for admins
            who want the official stage tree.
  runit     PID 1 is runit-init. Stages are /etc/runit/1, 2, 3.
            Enable with a symlink in /etc/runit/runsvdir/default.
  dinit     PID 1 is dinit. Enable by linking into /etc/dinit.d/boot.d.
            Halt/reboot wrappers are named dinit-halt and friends so
            they do not collide with busybox.
  shepherd  PID 1 is GNU Shepherd. Services are Scheme files in
            /etc/shepherd.d. This pulls Guile and Fibers.
  sysvinit  PID 1 is /sbin/init. Boot scripts live in /etc/rc.d.
            Enable with a symlink in /etc/rc.d/rc3.d (and friends).
            eudev and elogind come with init-sysvinit.
  none      no init package was requested. Busybox can still provide
            an init applet if you copy it to /sbin/init yourself.

setup enables known services for the init recorded in /etc/sps/init
(and the landed init-* package). Daemon packages ship scripts for
every init into the directories that init expects. Pass --services no
to skip enablement. Later sget install of a mapped package enables
it too.
EOF

	cat >"$setup_root/root/sps-network.txt" <<EOF
SPS setup does not write DHCP or static network configuration.

On a Plasma system, use plasma-nm after the first graphical login.
On a console system, configure the interface by hand, for example with
ip(8) and a simple /etc/network script of your own.

OpenSSH is installed when the server profile or the ssh set is selected.
EOF

	cat >"$setup_root/root/sps-fstab.txt" <<EOF
If setup partitioned the disk, /etc/fstab was written from blkid UUIDs.
Read it before the first boot. Add extra mounts yourself.

Partition layout: $setup_partition
Disk: ${setup_disk:-<none>}
Root: ${setup_part_root:-<none>}
EFI:  ${setup_part_efi:-<none>}
Swap: ${setup_part_swap:-<none>}
Home: ${setup_part_home:-<none>}
EOF

	if command -v setup_write_fstab >/dev/null 2>&1 &&
	   [ -n "$setup_part_root" ]; then
		setup_write_fstab "$setup_root" || :
	fi
}

# Index the live system's bundled recipe trees in place (read-only) and
# write repos.conf so src update during setup works offline. Live ISOs
# already have those trees at /usr/src/sps/{core,extra}; cloning GitHub
# during setup is what made "updating source repositories" hang or fail
# offline. The installed machine re-points to the official Git remotes so
# it keeps receiving recipe updates after install.
setup_tree_has_recipe()
{
	setup_probe=$1
	[ -d "$setup_probe" ] || return 1
	find "$setup_probe" \( -type d \( -name .git -o -name files -o \
		-name patches -o -name hooks \) -prune \) -o \
		-type f -name recipe -print | {
		IFS= read -r setup_probe_recipe || true
		[ -n "$setup_probe_recipe" ]
	}
}

setup_seed_one_tree()
{
	setup_seed_name=$1
	setup_seed_src=${setup_live_root%/}/usr/src/sps/$setup_seed_name
	setup_tree_has_recipe "$setup_seed_src" || return 1
	return 0
}

setup_write_repos_conf()
{
	setup_repos_prefix=$1
	setup_repos_mode=${2:-installed}
	setup_repos_core=/usr/src/sps/core
	setup_repos_extra=/usr/src/sps/extra
	if [ -n "$setup_repos_prefix" ] && [ "$setup_repos_prefix" != / ]; then
		setup_repos_core=$setup_repos_prefix$setup_repos_core
		setup_repos_extra=$setup_repos_prefix$setup_repos_extra
	fi
	{
		if [ "$setup_repos_mode" = seed ] &&
		   [ "${setup_have_core:-0}" -eq 1 ]; then
			printf 'dir core %s 100\n' "$setup_repos_core"
		else
			printf 'git core https://github.com/RobertFlexx/sps-core.git 100\n'
		fi
		if [ "$setup_repos_mode" = seed ] &&
		   [ "${setup_have_extra:-0}" -eq 1 ]; then
			printf 'dir extra %s 80\n' "$setup_repos_extra"
		else
			printf 'git extra https://github.com/RobertFlexx/sps-extra.git 80\n'
		fi
		sps_print_community_repos_hint
	} >"$setup_root/etc/sps/repos.conf" ||
		setup_fail "$SPS_EX_PERMISSION" "cannot write target repos.conf"
}

setup_seed_recipe_trees()
{
	setup_live_root=${SPS_LIVE_ROOT:-/}
	case $setup_live_root in
		/*) ;;
		*)
			setup_fail "$SPS_EX_USAGE" \
				"SPS_LIVE_ROOT must be an absolute path: $setup_live_root"
			;;
	esac
	[ "$setup_live_root" = / ] || setup_live_root=${setup_live_root%/}

	mkdir -p "$setup_root/etc/sps" "$setup_root/usr/src/sps" ||
		setup_fail "$SPS_EX_PERMISSION" "cannot create target repository directories"

	setup_have_core=0
	setup_have_extra=0
	if setup_seed_one_tree core; then
		setup_have_core=1
	fi
	if setup_seed_one_tree extra; then
		setup_have_extra=1
	fi

	# During setup, the live system's bundled trees must exist for src
	# update to index them in place. apply_config later re-points the
	# installed machine at the official Git remotes.
	setup_write_repos_conf "${setup_live_root%/}" seed

	if [ "$setup_have_core" -eq 1 ] && [ "$setup_have_extra" -eq 1 ]; then
		printf '%s: indexing live bundled recipes; the installed system tracks the official Git repositories\n' \
			"$sps_prog" >&2
	else
		printf '%s: missing bundled recipes; src update will clone from the network\n' \
			"$sps_prog" >&2
	fi
}

# Packages first, then hostname/user so filesystem's passwd is already present.
setup_root=${setup_target%/}
mkdir -p "$setup_root" ||
	setup_fail "$SPS_EX_PERMISSION" "cannot create target: $setup_root"

export SPS_ROOT=$setup_root
[ "$setup_keep_db" -eq 1 ] || unset SPS_DB
[ "$setup_keep_cache" -eq 1 ] || unset SPS_CACHE
[ "$setup_keep_build" -eq 1 ] || unset SPS_BUILD
[ "$setup_keep_repo_root" -eq 1 ] || unset SPS_REPO_ROOT
[ "$setup_keep_config" -eq 1 ] || unset SPS_CONFIG
[ "$setup_keep_repos" -eq 1 ] || unset SPS_REPOS_CONFIG

if [ "$setup_kernel" = custom ] && [ -n "$setup_kernel_config" ]; then
	mkdir -p "$setup_root/etc/sps"
	cp "$setup_kernel_config" "$setup_root/etc/sps/kernel.config" ||
		setup_fail "$SPS_EX_PERMISSION" "cannot copy kernel config"
fi

setup_seed_recipe_trees

# Hooks run during sget, before apply_config. Record the init choice so
# post-install can enable SDDM and friends for the chosen PID 1.
mkdir -p "$setup_root/etc/sps" ||
	setup_fail "$SPS_EX_PERMISSION" "cannot create $setup_root/etc/sps"
printf '%s\n' "${setup_init:-none}" >"$setup_root/etc/sps/init"

# sget/mkpkg read $SPS_ROOT/etc/sps/sps.conf once SPS_CONFIG is unset.
# Write it before the first package so makejobs/cflags apply to the
# source install, not only after apply_config.
if [ ! -f "$setup_root/etc/sps/sps.conf" ]; then
	if [ -f /etc/sps/live ] && [ -f /etc/sps/sps.conf ]; then
		cp /etc/sps/sps.conf "$setup_root/etc/sps/sps.conf" ||
			setup_fail "$SPS_EX_PERMISSION" "cannot copy live sps.conf"
	else
		sps_print_default_conf >"$setup_root/etc/sps/sps.conf" ||
			setup_fail "$SPS_EX_PERMISSION" "cannot write sps.conf"
	fi
fi

if [ "$setup_do_update" -eq 1 ]; then
	printf '%s: updating source repositories\n' "$sps_prog" >&2
	src update || setup_fail "$SPS_EX_NETWORK" "src update failed"
fi

setup_reject_unknown_packages

setup_pkgs=
while IFS= read -r setup_pkg; do
	[ -n "$setup_pkg" ] || continue
	setup_pkgs="$setup_pkgs $setup_pkg"
done <<EOF
$(sort -u "$setup_pkg_file")
EOF

printf '%s: installing packages:%s\n' "$sps_prog" "$setup_pkgs" >&2
# Fail before the first autoconf package if the live gcc cannot link.
# Missing libmpc (cc1) or crt1.o shows up as "C compiler cannot create
# executables" two screens later otherwise.
if command -v gcc >/dev/null 2>&1; then
	setup_cc_dir=${SPS_BUILD:-$setup_root/var/tmp/sps}
	mkdir -p "$setup_cc_dir" ||
		setup_fail "$SPS_EX_PERMISSION" "cannot create $setup_cc_dir"
	setup_cc_log=$setup_cc_dir/sps-cc-probe.log
	setup_cc_out=$setup_cc_dir/sps-cc-probe
	if ! printf '%s\n' 'int main(void){return 0;}' |
		gcc -x c - -o "$setup_cc_out" >"$setup_cc_log" 2>&1
	then
		printf '%s: live gcc cannot create executables\n' "$sps_prog" >&2
		cat "$setup_cc_log" >&2
		setup_fail "$SPS_EX_BUILD" "live gcc cannot create executables"
	fi
	rm -f "$setup_cc_out"
fi
# Meson recipes (libxml2 is the first for many desktop graphs) fail with
# a Python traceback when mesonbuild is missing, or with a meson error
# about option "python" when the recipe passes -Dpython=false.
if command -v meson >/dev/null 2>&1; then
	setup_meson_dir=${SPS_BUILD:-$setup_root/var/tmp/sps}
	mkdir -p "$setup_meson_dir" ||
		setup_fail "$SPS_EX_PERMISSION" "cannot create $setup_meson_dir"
	setup_meson_log=$setup_meson_dir/sps-meson-probe.log
	if ! meson --version >"$setup_meson_log" 2>&1; then
		printf '%s: live meson cannot run\n' "$sps_prog" >&2
		cat "$setup_meson_log" >&2
		setup_fail "$SPS_EX_BUILD" "live meson cannot run"
	fi
fi
# sget treats requested names as explicit world roots.
# shellcheck disable=SC2086
sget install $setup_pkgs ||
	setup_fail "$SPS_EX_BUILD" "package installation failed"

# Hostname, user, and notes land after packages so filesystem's passwd wins.
setup_apply_config

if [ "$setup_services" = yes ] &&
   command -v setup_service_enable_present >/dev/null 2>&1
then
	case $setup_init in
		systemd|openrc|s6|runit|dinit|shepherd|sysvinit)
			printf '%s: enabling services after user configuration, init %s\n' \
				"$sps_prog" "$setup_init" >&2
			setup_svc_list=$(setup_service_enable_present \
				"$setup_root" "$setup_init") || setup_svc_list=
			{
				printf '%s\n' \
					"Init: $setup_init" \
					"setup enables known services that were installed." \
					"Daemon packages already contain scripts for this init." \
					"Missing files are filled from stock copies" \
					"in $SPS_LIBDIR/setup/sv/$setup_init." \
					"It does not start daemons inside a target chroot." \
					""
				if [ -n "$setup_svc_list" ]; then
					printf '%s\n' 'Enabled:'
					printf '%s\n' "$setup_svc_list" | sort -u
				else
					printf '%s\n' \
						'No mapped service could be enabled in the target.' \
						'Install the package and rerun, or enable it yourself after first boot.'
				fi
			} >"$setup_root/root/sps-services.txt"
			;;
		*)
			cat >"$setup_root/root/sps-services.txt" <<EOF
Init is ${setup_init:-none}. setup did not enable services.

To have SDDM, sshd, NetworkManager, and similar start at boot, rerun
setup with --init systemd, openrc, s6, runit, dinit, shepherd, or sysvinit, or
enable them yourself.
EOF
			;;
	esac
fi

# On a live ISO, rewrite of repos.conf pointed at /usr/src/sps. Rebuild
# the index so the installed system does not keep /mnt-prefixed recipe paths.
if [ "$setup_do_update" -eq 1 ] && [ -f /etc/sps/live ]; then
	printf '%s: reindexing recipes for the installed system\n' "$sps_prog" >&2
	src update || printf '%s: installed-system reindex failed; run src update after boot\n' \
		"$sps_prog" >&2
fi

if [ "$setup_flatpak" = yes ]; then
	setup_fp=$setup_root/usr/bin/flatpak
	if [ -x "$setup_fp" ]; then
		printf '%s: adding Flathub remote\n' "$sps_prog" >&2
		"$setup_fp" --installation=system remote-add --if-not-exists flathub \
			https://dl.flathub.org/repo/flathub.flatpakrepo \
			--user=false 2>/dev/null ||
			SPS_ROOT=$setup_root "$setup_fp" remote-add --if-not-exists \
				flathub https://dl.flathub.org/repo/flathub.flatpakrepo ||
			printf '%s: could not add Flathub; add it after first boot\n' \
				"$sps_prog" >&2
	else
		printf '%s: flatpak is not in the target yet; Flathub skipped\n' \
			"$sps_prog" >&2
	fi
fi

if [ "$setup_install_bootloader" -eq 1 ]; then
	command -v grub-install >/dev/null 2>&1 ||
		setup_fail "$SPS_EX_PACKAGE" "grub-install is not available"
	[ -n "$setup_disk" ] ||
		setup_fail "$SPS_EX_USAGE" \
			"--install-bootloader requires --disk; refusing to guess. See /root/sps-bootloader.txt"
	printf '%s: grub-install %s (explicit)\n' "$sps_prog" "$setup_disk" >&2
	if [ -d "$setup_root/boot/efi" ]; then
		# Firmware fallback path: \EFI\BOOT\BOOTX64.EFI. This does not
		# need efibootmgr or working NVRAM, which QEMU often lacks.
		printf '%s: grub-install EFI removable (EFI/BOOT/BOOTX64.EFI)\n' \
			"$sps_prog" >&2
		grub-install --target=x86_64-efi \
			--efi-directory="$setup_root/boot/efi" \
			--boot-directory="$setup_root/boot" \
			--removable --no-nvram \
			"$setup_disk" ||
			setup_fail "$SPS_EX_BUILD" "grub-install (EFI removable) failed"
		if command -v efibootmgr >/dev/null 2>&1; then
			printf '%s: grub-install EFI Boot#### via efibootmgr\n' \
				"$sps_prog" >&2
			grub-install --target=x86_64-efi \
				--efi-directory="$setup_root/boot/efi" \
				--bootloader-id=SPS \
				--boot-directory="$setup_root/boot" \
				"$setup_disk" ||
				printf '%s: efibootmgr NVRAM update failed; EFI/BOOT/BOOTX64.EFI is in place\n' \
					"$sps_prog" >&2
		fi
	else
		grub-install --target=i386-pc --boot-directory="$setup_root/boot" \
			"$setup_disk" ||
			setup_fail "$SPS_EX_BUILD" "grub-install (BIOS) failed"
	fi
fi

setup_msg "Installation finished.

Target: $setup_root
Answers: $setup_root/etc/sps/setup-answers
Bootloader notes: $setup_root/root/sps-bootloader.txt

Set passwords before the first login. If you selected NVIDIA, rebuild the
kernel modules against the installed kernel."

exit 0