Splux Linux

source-based Linux using SPS

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

e045da0


setup: confirm guided format and enable init-aware services

Date
Commite045da0aaab1c7edd0726fd0aad8dc01ec0fbb84
GitHubmirror

Parents

Stat

 Makefile                 |   1 +
 bin/mkiso                |  23 +++-
 bin/setup                | 236 ++++++++++++++++++++++++++++++++++++---
 lib/mkiso/live-plasma    |  44 ++++++--
 lib/mkiso/live-rc        |  35 ++++++
 lib/mkiso/plasma-bins    |   2 +
 lib/setup/service.sh     | 282 +++++++++++++++++++++++++++++++++++++++++++++++
 lib/setup/services       |  17 +++
 lib/setup/sets/power.set |   1 -
 man/setup.1              |  44 +++++++-
 tests/test_mkiso.sh      |  18 +++
 tests/test_services.sh   |  78 +++++++++++++
 tests/test_setup.sh      |  69 ++++++++++++
 13 files changed, 815 insertions(+), 35 deletions(-)

Diff

diff --git a/Makefile b/Makefile
index 9e1785a..bc01a7d 100644
--- a/Makefile
+++ b/Makefile
@@ -161,6 +161,7 @@ install:
 		"$(DESTDIR)$(LIBDIR)/setup/sets"
 	install -m 0644 lib/setup/keymaps lib/setup/timezones \
 		lib/setup/locales lib/setup/shells lib/setup/extras \
+		lib/setup/services \
 		"$(DESTDIR)$(LIBDIR)/setup/"
 	@set -e; for f in lib/setup/profiles/*.profile; do \
 		install -m 0644 $$f "$(DESTDIR)$(LIBDIR)/setup/profiles/"; \
diff --git a/bin/mkiso b/bin/mkiso
index 49056cb..ac02cc2 100755
--- a/bin/mkiso
+++ b/bin/mkiso
@@ -510,6 +510,27 @@ mkiso_copy_plasma()
 			mkiso_copy_path "$mkiso_so"
 		done
 	done
+	# Host /etc/dbus-1 may already contain system-local.conf. live-plasma
+	# used to skip the root-user override when that file existed, which
+	# left dbus-daemon --system looking for a messagebus account the live
+	# passwd does not have.
+	mkdir -p "$mkiso_live/etc/dbus-1"
+	cat >"$mkiso_live/etc/dbus-1/system-local.conf" <<'EOF'
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+  <user>root</user>
+</busconfig>
+EOF
+	for mkiso_cmd in dbus-daemon dbus-run-session dbus-launch dbus-uuidgen \
+		dbus-send
+	do
+		mkiso_copy_host_cmd "$mkiso_cmd" || :
+	done
+	mkiso_copy_path /usr/libexec/dbus-daemon-launch-helper
+	mkdir -p "$mkiso_live/etc/systemd/system"
+	ln -sf /dev/null "$mkiso_live/etc/systemd/system/dbus.service"
+	ln -sf /dev/null "$mkiso_live/etc/systemd/system/dbus.socket"
 }
 
 # PID 1 must be an ELF. A shell script that execs getty/setsid is what
@@ -717,7 +738,7 @@ if [ -f "$mkiso_libdir/live-bins" ]; then
 fi
 printf '%s: copying disk tools for setup\n' "$sps_prog" >&2
 for mkiso_cmd in lsblk blkid sfdisk cfdisk fdisk wipefs mkfs.ext4 mke2fs \
-	mkfs.vfat mkswap partprobe udevadm udevd
+	mkfs.vfat mkswap partprobe udevadm udevd grub-install grub-mkconfig
 do
 	mkiso_copy_host_cmd "$mkiso_cmd" || :
 done
diff --git a/bin/setup b/bin/setup
index 3be9cd8..53e0431 100755
--- a/bin/setup
+++ b/bin/setup
@@ -67,12 +67,13 @@ options:
   --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     PID 1 on the installed system (default: none in
                             scripts; systemd on the interactive screen)
-  --bootloader none|grub    write bootloader notes; never runs grub-install
+  --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
@@ -95,6 +96,10 @@ 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
 setup_work=
 setup_interactive=1
 setup_plan=0
@@ -129,6 +134,7 @@ setup_kernel=
 setup_kernel_config=
 setup_drivers=
 setup_init=
+setup_services=
 setup_part_efi=
 setup_part_root=
 setup_part_swap=
@@ -322,6 +328,11 @@ while [ "$#" -gt 0 ]; do
 			setup_init=$2
 			shift 2
 			;;
+		--services)
+			setup_need_value "$@"
+			setup_services=$2
+			shift 2
+			;;
 		--list-hardware)
 			setup_list_hardware=1
 			shift
@@ -545,7 +556,12 @@ setup_load_answers()
 			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)
@@ -642,20 +658,19 @@ EOF
 
 setup_ask_partition()
 {
-	if [ -n "$setup_partition" ]; then
-		return 0
+	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
-	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"
 
 	case $setup_partition in
 		none|existing)
@@ -666,6 +681,29 @@ setup_ask_partition()
 				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
 
@@ -983,12 +1021,35 @@ The live image and auto drivers use nouveau. This extra set is the proprietary/o
 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 runs grub-install unless you pass --install-bootloader with --disk. The usual choice is to write notes only." \
-		14 64 3 \
+		--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)") ||
+		grub "Write GRUB notes (still does not install)" \
+		install "Install GRUB to the chosen disk (asks again)") ||
 		setup_fail "$SPS_EX_USAGE" "cancelled"
-	setup_bootloader=$setup_bl
+	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
@@ -1008,6 +1069,7 @@ setup_printing=$(setup_yesno_norm "${setup_printing:-no}")
 [ -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
@@ -1023,6 +1085,10 @@ case $setup_init in
 	none|systemd|openrc) ;;
 	*) setup_fail "$SPS_EX_USAGE" "init must be systemd, openrc, 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" ;;
@@ -1039,6 +1105,14 @@ case $setup_partition in
 			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" ] ||
@@ -1055,6 +1129,16 @@ if [ "$setup_kernel" = custom ]; then
 	[ -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" ;;
@@ -1159,6 +1243,55 @@ 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
+	case $setup_init in
+		systemd)
+			case $setup_item in
+				elogind|eudev|openrc|init-openrc)
+					setup_fail "$SPS_EX_CONFLICT" \
+						"cannot combine --init systemd with $setup_item (elogind/eudev/OpenRC conflict with systemd)"
+					;;
+			esac
+			;;
+		openrc)
+			case $setup_item in
+				systemd|init-systemd)
+					setup_fail "$SPS_EX_CONFLICT" \
+						"cannot combine --init openrc with $setup_item"
+					;;
+			esac
+			;;
+	esac
+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
+	setup_drop=0
+	case $setup_init in
+		systemd)
+			case $setup_pkg in
+				elogind|eudev|openrc|init-openrc) setup_drop=1 ;;
+			esac
+			;;
+		openrc)
+			case $setup_pkg in
+				systemd|init-systemd) setup_drop=1 ;;
+			esac
+			;;
+	esac
+	if [ "$setup_drop" -eq 1 ]; 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 ;;
@@ -1210,6 +1343,9 @@ setup_write_answers_file()
 		[ -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"
@@ -1242,6 +1378,23 @@ setup_summary()
 	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 or OpenRC.\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"
 }
@@ -1383,6 +1536,10 @@ packages land:
            elogind are in the init-openrc set.
   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 (SDDM, dbus, sshd, NetworkManager, ...)
+for the chosen init when those packages installed a unit or OpenRC
+script. See /root/sps-services.txt. Pass --services no to skip that.
 EOF
 
 	cat >"$setup_root/root/sps-network.txt" <<EOF
@@ -1538,6 +1695,12 @@ 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 systemd or OpenRC.
+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"
+
 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"
@@ -1560,6 +1723,43 @@ sget install $setup_pkgs ||
 # 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)
+			printf '%s: enabling services for 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." \
+					"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 unit or OpenRC script was present in the target.' \
+						'Install the package files (for example sddm.service) and rerun,' \
+						'or enable the service 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 or --init openrc, 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
diff --git a/lib/mkiso/live-plasma b/lib/mkiso/live-plasma
index 4977c8a..4ebb2f8 100644
--- a/lib/mkiso/live-plasma
+++ b/lib/mkiso/live-plasma
@@ -43,21 +43,43 @@ if [ ! -e /var/lib/dbus/machine-id ] && [ -s /etc/machine-id ]; then
 fi
 # The stock system.conf drops privileges to messagebus. The live passwd
 # has no such account, so dbus-daemon --system exits and Plasma cannot
-# talk to the bus. Override that for this disc only.
-if [ ! -f /etc/dbus-1/system-local.conf ]; then
-	printf '%s\n' \
-		'<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"' \
-		' "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">' \
-		'<busconfig>' \
-		'  <user>root</user>' \
-		'</busconfig>' >/etc/dbus-1/system-local.conf
-fi
+# talk to the bus. Always rewrite this: mkiso copies host /etc/dbus-1,
+# which may already contain a system-local.conf without <user>root</user>.
+mkdir -p /etc/dbus-1 /run/dbus /var/lib/dbus
+printf '%s\n' \
+	'<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"' \
+	' "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">' \
+	'<busconfig>' \
+	'  <user>root</user>' \
+	'</busconfig>' >/etc/dbus-1/system-local.conf
 
 if command -v dbus-daemon >/dev/null 2>&1; then
 	if [ ! -S /run/dbus/system_bus_socket ] &&
-	   [ ! -S /var/run/dbus/system_bus_socket ]; then
-		dbus-daemon --system --fork 2>/dev/null || :
+	   [ ! -S /var/run/dbus/system_bus_socket ]
+	then
+		dbus-daemon --system --nofork --nopidfile --nosyslog \
+			>/run/sps-dbus.log 2>&1 &
+		n=0
+		while [ "$n" -lt 8 ]; do
+			if [ -S /run/dbus/system_bus_socket ] ||
+			   [ -S /var/run/dbus/system_bus_socket ]
+			then
+				break
+			fi
+			n=$((n + 1))
+			sleep 1
+		done
+	fi
+	if [ ! -S /run/dbus/system_bus_socket ] &&
+	   [ ! -S /var/run/dbus/system_bus_socket ]
+	then
+		printf '%s\n' "SPS: system D-Bus is not running; Plasma will fail."
+		printf '%s\n' "SPS: dbus-daemon log:"
+		cat /run/sps-dbus.log 2>/dev/null || :
+		printf '%s\n' "SPS: use tty2 (Ctrl-Alt-F2) for a shell."
 	fi
+else
+	printf '%s\n' "SPS: dbus-daemon is not on PATH; Plasma needs it."
 fi
 if command -v seatd >/dev/null 2>&1; then
 	seatd -g video >/dev/null 2>&1 &
diff --git a/lib/mkiso/live-rc b/lib/mkiso/live-rc
index 6199c2c..aebfeea 100644
--- a/lib/mkiso/live-rc
+++ b/lib/mkiso/live-rc
@@ -88,6 +88,41 @@ elif [ -x /lib/systemd/systemd-udevd ]; then
 	udevadm settle --timeout=5 2>/dev/null || :
 fi
 
+# System bus before Plasma. Always rewrite system-local.conf: a copied
+# host file would otherwise keep <user>messagebus</user>.
+mkdir -p /run/dbus /var/lib/dbus /etc/dbus-1
+cat >/etc/dbus-1/system-local.conf <<'EOF'
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+  <user>root</user>
+</busconfig>
+EOF
+if [ ! -S /run/dbus/system_bus_socket ] &&
+   [ ! -S /var/run/dbus/system_bus_socket ]
+then
+	if command -v dbus-daemon >/dev/null 2>&1; then
+		dbus-daemon --system --nofork --nopidfile --nosyslog \
+			>/run/sps-dbus.log 2>&1 &
+		n=0
+		while [ "$n" -lt 8 ]; do
+			if [ -S /run/dbus/system_bus_socket ] ||
+			   [ -S /var/run/dbus/system_bus_socket ]
+			then
+				break
+			fi
+			n=$((n + 1))
+			sleep 1
+		done
+		if [ ! -S /run/dbus/system_bus_socket ] &&
+		   [ ! -S /var/run/dbus/system_bus_socket ]
+		then
+			printf '%s\n' "SPS: dbus-daemon did not create a system bus socket"
+			cat /run/sps-dbus.log 2>/dev/null || :
+		fi
+	fi
+fi
+
 cat /etc/issue 2>/dev/null || :
 cat /etc/motd 2>/dev/null || :
 
diff --git a/lib/mkiso/plasma-bins b/lib/mkiso/plasma-bins
index a83ce5a..8586b56 100644
--- a/lib/mkiso/plasma-bins
+++ b/lib/mkiso/plasma-bins
@@ -27,6 +27,8 @@
 /usr/bin/dbus-launch
 /usr/bin/dbus-run-session
 /usr/bin/dbus-uuidgen
+/usr/bin/dbus-send
+/usr/libexec/dbus-daemon-launch-helper
 /usr/bin/seatd
 /usr/bin/Xorg
 /usr/bin/xauth
diff --git a/lib/setup/service.sh b/lib/setup/service.sh
new file mode 100644
index 0000000..6ee2a36
--- /dev/null
+++ b/lib/setup/service.sh
@@ -0,0 +1,282 @@
+# Init-aware service enablement for the SPS Linux installer.
+#
+# Sourced, not executed. setup(1) and package post-install hooks call these
+# helpers. They never invent an init; they read /etc/sps/init (or the
+# hook environment) and only enable units/scripts that already exist in
+# the target. Enablement is on-boot; start happens only when the target
+# is the running system and PID 1 matches.
+
+setup_service_map_file()
+{
+	if [ -n "${setup_dir-}" ] && [ -f "$setup_dir/services" ]; then
+		printf '%s\n' "$setup_dir/services"
+		return 0
+	fi
+	if [ -n "${SPS_LIBDIR-}" ] && [ -f "$SPS_LIBDIR/setup/services" ]; then
+		printf '%s\n' "$SPS_LIBDIR/setup/services"
+		return 0
+	fi
+	return 1
+}
+
+setup_service_lookup()
+{
+	# $1 = package name. Prints: unit wanted_by rc_name rc_level
+	setup_svc_pkg=$1
+	setup_svc_map=$(setup_service_map_file) || return 1
+	awk -v pkg="$setup_svc_pkg" '
+		BEGIN { found = 0 }
+		$1 == "#" || $1 == "" { next }
+		$1 == pkg {
+			unit = $2
+			wanted = $3
+			rc = $4
+			level = $5
+			if (unit == "-" || unit == ".") unit = ""
+			if (wanted == "-" || wanted == ".") wanted = ""
+			if (rc == "-" || rc == ".") rc = ""
+			if (level == "-" || level == ".") level = "default"
+			print unit, wanted, rc, level
+			found = 1
+			exit
+		}
+		END { exit !found }
+	' "$setup_svc_map"
+}
+
+setup_service_root()
+{
+	setup_svc_root=${1:-${SPS_ROOT:-/}}
+	[ -n "$setup_svc_root" ] || setup_svc_root=/
+	[ "$setup_svc_root" = / ] && return 0
+	printf '%s\n' "${setup_svc_root%/}"
+}
+
+setup_service_init_of()
+{
+	setup_svc_root=$(setup_service_root "$1")
+	if [ -z "$setup_svc_root" ]; then
+		setup_svc_initf=/etc/sps/init
+	else
+		setup_svc_initf=$setup_svc_root/etc/sps/init
+	fi
+	if [ -f "$setup_svc_initf" ]; then
+		sed -n '1p' "$setup_svc_initf"
+		return 0
+	fi
+	printf '%s\n' none
+}
+
+setup_service_unit_path()
+{
+	setup_svc_root=$(setup_service_root "$1")
+	setup_svc_unit=$2
+	[ -n "$setup_svc_unit" ] || return 1
+	for setup_svc_p in \
+		"$setup_svc_root/usr/lib/systemd/system/$setup_svc_unit" \
+		"$setup_svc_root/lib/systemd/system/$setup_svc_unit" \
+		"$setup_svc_root/etc/systemd/system/$setup_svc_unit"
+	do
+		[ -f "$setup_svc_p" ] || continue
+		printf '%s\n' "$setup_svc_p"
+		return 0
+	done
+	return 1
+}
+
+setup_service_rc_path()
+{
+	setup_svc_root=$(setup_service_root "$1")
+	setup_svc_rc=$2
+	[ -n "$setup_svc_rc" ] || return 1
+	if [ -z "$setup_svc_root" ]; then
+		setup_svc_p=/etc/init.d/$setup_svc_rc
+	else
+		setup_svc_p=$setup_svc_root/etc/init.d/$setup_svc_rc
+	fi
+	[ -f "$setup_svc_p" ] || [ -x "$setup_svc_p" ] || return 1
+	printf '%s\n' "$setup_svc_p"
+}
+
+setup_service_enable_systemd()
+{
+	setup_svc_root=$(setup_service_root "$1")
+	setup_svc_unit=$2
+	setup_svc_wanted=${3:-multi-user.target}
+	setup_svc_path=$(setup_service_unit_path "$1" "$setup_svc_unit") ||
+		return 1
+	setup_svc_wdir=
+	if [ -z "$setup_svc_root" ]; then
+		setup_svc_wdir=/etc/systemd/system/${setup_svc_wanted}.wants
+		setup_svc_link=/usr/lib/systemd/system/$setup_svc_unit
+		[ -f "$setup_svc_path" ] &&
+			case $setup_svc_path in
+				/etc/*) setup_svc_link=$setup_svc_path ;;
+				/lib/*) setup_svc_link=$setup_svc_path ;;
+				/usr/*) setup_svc_link=$setup_svc_path ;;
+			esac
+	else
+		setup_svc_wdir=$setup_svc_root/etc/systemd/system/${setup_svc_wanted}.wants
+		case $setup_svc_path in
+			"$setup_svc_root"/*)
+				setup_svc_link=${setup_svc_path#"$setup_svc_root"}
+				;;
+			*)
+				setup_svc_link=/usr/lib/systemd/system/$setup_svc_unit
+				;;
+		esac
+	fi
+	mkdir -p "$setup_svc_wdir" || return 1
+	ln -sf "$setup_svc_link" "$setup_svc_wdir/$setup_svc_unit" || return 1
+	if [ "$setup_svc_wanted" = graphical.target ]; then
+		if [ -z "$setup_svc_root" ]; then
+			setup_svc_def=/etc/systemd/system/default.target
+		else
+			setup_svc_def=$setup_svc_root/etc/systemd/system/default.target
+		fi
+		if [ ! -e "$setup_svc_def" ]; then
+			ln -sf /usr/lib/systemd/system/graphical.target \
+				"$setup_svc_def" || :
+		fi
+	fi
+	return 0
+}
+
+setup_service_enable_openrc()
+{
+	setup_svc_root=$(setup_service_root "$1")
+	setup_svc_rc=$2
+	setup_svc_level=${3:-default}
+	setup_service_rc_path "$1" "$setup_svc_rc" >/dev/null || return 1
+	if [ -z "$setup_svc_root" ]; then
+		setup_svc_rdir=/etc/runlevels/$setup_svc_level
+	else
+		setup_svc_rdir=$setup_svc_root/etc/runlevels/$setup_svc_level
+	fi
+	mkdir -p "$setup_svc_rdir" || return 1
+	ln -sf "/etc/init.d/$setup_svc_rc" "$setup_svc_rdir/$setup_svc_rc"
+}
+
+setup_service_is_live_root()
+{
+	setup_svc_root=$(setup_service_root "$1")
+	[ -z "$setup_svc_root" ]
+}
+
+setup_service_pid1()
+{
+	if [ -d /run/systemd/system ]; then
+		printf '%s\n' systemd
+		return 0
+	fi
+	if [ -d /run/openrc ]; then
+		printf '%s\n' openrc
+		return 0
+	fi
+	printf '%s\n' none
+}
+
+setup_service_maybe_start()
+{
+	# Only start when installing onto the running machine.
+	setup_service_is_live_root "$1" || return 0
+	setup_svc_init=$2
+	setup_svc_unit=$3
+	setup_svc_rc=$4
+	setup_svc_pid1=$(setup_service_pid1)
+	[ "$setup_svc_pid1" = "$setup_svc_init" ] || return 0
+	case $setup_svc_init in
+		systemd)
+			[ -n "$setup_svc_unit" ] || return 0
+			command -v systemctl >/dev/null 2>&1 || return 0
+			systemctl start "${setup_svc_unit%.service}" 2>/dev/null ||
+				systemctl start "$setup_svc_unit" 2>/dev/null || :
+			;;
+		openrc)
+			[ -n "$setup_svc_rc" ] || return 0
+			if command -v rc-service >/dev/null 2>&1; then
+				rc-service "$setup_svc_rc" start 2>/dev/null || :
+			fi
+			;;
+	esac
+	return 0
+}
+
+setup_service_enable_one()
+{
+	# $1 root  $2 init  $3 package
+	setup_svc_root=$1
+	setup_svc_init=$2
+	setup_svc_pkg=$3
+	setup_svc_fields=$(setup_service_lookup "$setup_svc_pkg") || return 1
+	set -- $setup_svc_fields
+	setup_svc_unit=$1
+	setup_svc_wanted=$2
+	setup_svc_rc=$3
+	setup_svc_level=$4
+	setup_svc_did=0
+	case $setup_svc_init in
+		systemd)
+			if [ -n "$setup_svc_unit" ] &&
+			   setup_service_enable_systemd "$setup_svc_root" \
+				"$setup_svc_unit" "$setup_svc_wanted"
+			then
+				setup_svc_did=1
+			fi
+			;;
+		openrc)
+			if [ -n "$setup_svc_rc" ] &&
+			   setup_service_enable_openrc "$setup_svc_root" \
+				"$setup_svc_rc" "$setup_svc_level"
+			then
+				setup_svc_did=1
+			fi
+			;;
+		*)
+			return 1
+			;;
+	esac
+	[ "$setup_svc_did" -eq 1 ] || return 1
+	setup_service_maybe_start "$setup_svc_root" "$setup_svc_init" \
+		"$setup_svc_unit" "$setup_svc_rc"
+	printf '%s\n' "$setup_svc_pkg"
+	return 0
+}
+
+setup_service_enable_present()
+{
+	# Enable every mapped service whose unit/script is already in $1.
+	setup_svc_root=$1
+	setup_svc_init=$2
+	setup_svc_map=$(setup_service_map_file) || return 0
+	while IFS= read -r setup_svc_line || [ -n "$setup_svc_line" ]; do
+		case $setup_svc_line in
+			''|\#*) continue ;;
+		esac
+		setup_svc_pkg=${setup_svc_line%%[[:space:]]*}
+		[ -n "$setup_svc_pkg" ] || continue
+		if setup_service_enable_one "$setup_svc_root" "$setup_svc_init" \
+			"$setup_svc_pkg" >/dev/null
+		then
+			printf '%s\n' "$setup_svc_pkg"
+		fi
+	done <"$setup_svc_map"
+	return 0
+}
+
+setup_service_hook_enable()
+{
+	# Called from package post-install hooks. SPS_PACKAGE and SPS_ROOT
+	# are set by pkin(1). A missing map or unknown package is not an error.
+	setup_svc_pkg=${SPS_PACKAGE-}
+	[ -n "$setup_svc_pkg" ] || return 0
+	setup_svc_root=${SPS_ROOT:-/}
+	setup_svc_init=$(setup_service_init_of "$setup_svc_root")
+	case $setup_svc_init in
+		systemd|openrc) ;;
+		*) return 0 ;;
+	esac
+	setup_service_enable_one "$setup_svc_root" "$setup_svc_init" \
+		"$setup_svc_pkg" >/dev/null || :
+	return 0
+}
diff --git a/lib/setup/services b/lib/setup/services
new file mode 100644
index 0000000..2a2b96f
--- /dev/null
+++ b/lib/setup/services
@@ -0,0 +1,17 @@
+# package  systemd-unit  wanted-by  openrc-name  openrc-runlevel
+# "-" means this init has no file for that package.
+dbus	dbus.service	multi-user.target	dbus	sysinit
+sddm	sddm.service	graphical.target	sddm	default
+networkmanager	NetworkManager.service	multi-user.target	NetworkManager	default
+openssh	sshd.service	multi-user.target	sshd	default
+cups	cups.service	multi-user.target	cupsd	default
+bluez	bluetooth.service	multi-user.target	bluetooth	default
+iwd	iwd.service	multi-user.target	iwd	default
+dhcpcd	dhcpcd.service	multi-user.target	dhcpcd	default
+seatd	seatd.service	multi-user.target	seatd	default
+elogind	elogind.service	multi-user.target	elogind	sysinit
+power-profiles-daemon	power-profiles-daemon.service	multi-user.target	power-profiles-daemon	default
+bluetooth	bluetooth.service	multi-user.target	bluetooth	default
+sshd	sshd.service	multi-user.target	sshd	default
+cupsd	cups.service	multi-user.target	cupsd	default
+NetworkManager	NetworkManager.service	multi-user.target	NetworkManager	default
diff --git a/lib/setup/sets/power.set b/lib/setup/sets/power.set
index e584422..9795350 100644
--- a/lib/setup/sets/power.set
+++ b/lib/setup/sets/power.set
@@ -5,4 +5,3 @@ default     1
 package     upower
 package     power-profiles-daemon
 package     polkit
-package     elogind
diff --git a/man/setup.1 b/man/setup.1
index 54d7abc..98232da 100644
--- a/man/setup.1
+++ b/man/setup.1
@@ -36,14 +36,29 @@ The installer does not run
 unless
 .B --install-bootloader
 is given with
+.B --disk
+, or the interactive bootloader screen is used to install GRUB and the
+disk is confirmed a second time.
+Guided partitioning asks for a format confirmation in the dialog, then
+waits until the last summary before it writes the disk. Scripts still
+need
+.B --format yes
+with
 .B --disk .
-Guided partitioning waits until the last confirmation.
 .B --init systemd
 or
 .B --init openrc
 selects PID 1;
 .B sget
 will not install both.
+After packages land,
+.B setup
+enables known services (SDDM, dbus, sshd, NetworkManager, and others
+listed in
+.I $SPS_LIBDIR/setup/services )
+for that init. Pass
+.B --services no
+to skip enablement.
 Live images prefer systemd. If the build host has no systemd binaries,
 busybox init runs instead and respawns gettys on tty1 and ttyS0.
 .PP
@@ -150,7 +165,22 @@ Install the Flatpak set and attempt to add Flathub after packages land.
 .B --init systemd|openrc
 PID 1 on the installed system. Scripts default to none so a fixture
 install does not pull systemd. The interactive screen defaults to
-systemd.
+systemd. The power set does not pull elogind; OpenRC gets elogind from
+.B init-openrc
+so a Plasma install can use systemd without a conflict.
+.TP
+.B --services yes|no
+Enable known services for the chosen init after packages install
+(default yes). Does not start daemons inside a target chroot; first
+boot starts them. Package post-install hooks do the same for later
+.B sget install
+of extras such as sddm.
+.TP
+.B --format yes|no
+Required
+.B yes
+for non-interactive guided layouts. The dialog asks this after you pick
+a disk.
 .TP
 .B --nvidia yes|no
 Install NVIDIA open kernel modules and utilities. A matching kernel is
@@ -160,7 +190,12 @@ required; this installer does not build one.
 Write notes only.
 .TP
 .B --install-bootloader
-Refuses to guess a disk. See the notes file.
+Run
+.B grub-install
+on
+.B --disk .
+Refuses to guess a disk. The interactive screen offers the same step
+behind a second confirmation.
 .TP
 .B --no-update
 Skip
@@ -187,7 +222,8 @@ Options override values from the file.
 .SH FILES
 .TP
 .I $SPS_LIBDIR/setup
-Profiles, sets, keymaps, and timezones.
+Profiles, sets, keymaps, timezones, extra package list, and the
+service map.
 .TP
 .I $SPS_SETUP_DIR
 Optional override of that directory, used by tests.
diff --git a/tests/test_mkiso.sh b/tests/test_mkiso.sh
index daea353..fc31f53 100755
--- a/tests/test_mkiso.sh
+++ b/tests/test_mkiso.sh
@@ -119,6 +119,24 @@ if command -v lsblk >/dev/null 2>&1; then
 fi
 grep -q 'copying disk tools for setup' "$mkiso" ||
 	fail 'mkiso must copy partition tools by command name'
+grep -q grub-install "$mkiso" ||
+	fail 'mkiso must copy grub-install by command name for setup'
+grep -q 'system-local.conf' "$mkiso" ||
+	fail 'mkiso must bake a live D-Bus system-local.conf'
+grep -q '<user>root</user>' "$project_dir/lib/mkiso/live-plasma" ||
+	fail 'live-plasma must run the system bus as root'
+if grep -q 'if \[ ! -f /etc/dbus-1/system-local.conf \]' \
+	"$project_dir/lib/mkiso/live-plasma"
+then
+	fail 'live-plasma must overwrite host system-local.conf, not skip it'
+fi
+grep -q 'dbus-daemon --system --nofork --nopidfile --nosyslog' \
+	"$project_dir/lib/mkiso/live-rc" ||
+	fail 'live-rc must start a system bus before Plasma'
+grep -q dbus-send "$project_dir/lib/mkiso/plasma-bins" ||
+	fail 'plasma-bins must include dbus-send'
+grep -q dbus-daemon-launch-helper "$project_dir/lib/mkiso/plasma-bins" ||
+	fail 'plasma-bins must include dbus-daemon-launch-helper'
 if [ -e "$layout/lib64/libtinfo.so.6" ] || [ -e "$layout/usr/lib64/libtinfo.so.6" ]
 then
 	for tinfo in "$layout/lib64/libtinfo.so.6" "$layout/usr/lib64/libtinfo.so.6"
diff --git a/tests/test_services.sh b/tests/test_services.sh
new file mode 100644
index 0000000..5c0db97
--- /dev/null
+++ b/tests/test_services.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+# setup service map: enable systemd units and OpenRC scripts in a fake root.
+
+set -eu
+
+case $0 in */*) test_dir=${0%/*} ;; *) test_dir=. ;; esac
+project_dir=$(CDPATH= cd "$test_dir/.." 2>/dev/null && pwd) || exit 1
+fail() { printf 'test_services: %s\n' "$*" >&2; exit 1; }
+
+export SPS_LIBDIR=$project_dir/lib
+# shellcheck disable=SC1091
+. "$project_dir/lib/setup/service.sh"
+
+tmp=$(mktemp -d "${TMPDIR:-/tmp}/sps-test-services.XXXXXX") || fail mktemp
+trap 'rm -rf "$tmp"' 0 HUP INT TERM
+
+map=$(setup_service_map_file) || fail 'service map missing'
+[ -f "$map" ] || fail "service map is not a file: $map"
+
+fields=$(setup_service_lookup sddm) || fail 'sddm is not in the service map'
+case $fields in
+	'sddm.service graphical.target sddm default') ;;
+	*) fail "unexpected sddm map fields: $fields" ;;
+esac
+
+root=$tmp/root
+mkdir -p "$root/usr/lib/systemd/system" "$root/etc/init.d" "$root/etc/sps" \
+	"$root/root"
+printf '%s\n' systemd >"$root/etc/sps/init"
+printf '%s\n' '[Unit]' >"$root/usr/lib/systemd/system/sddm.service"
+printf '%s\n' '[Unit]' >"$root/usr/lib/systemd/system/dbus.service"
+printf '%s\n' '#!/bin/sh' >"$root/etc/init.d/sddm"
+printf '%s\n' '#!/bin/sh' >"$root/etc/init.d/dbus"
+chmod 755 "$root/etc/init.d/sddm" "$root/etc/init.d/dbus"
+
+enabled=$(setup_service_enable_one "$root" systemd sddm) ||
+	fail 'systemd enable of sddm failed'
+[ "$enabled" = sddm ] || fail "enable_one printed '$enabled'"
+[ -L "$root/etc/systemd/system/graphical.target.wants/sddm.service" ] ||
+	fail 'sddm.service was not linked into graphical.target.wants'
+[ -L "$root/etc/systemd/system/default.target" ] ||
+	fail 'sddm should set default.target to graphical.target'
+
+enabled=$(setup_service_enable_one "$root" openrc sddm) ||
+	fail 'openrc enable of sddm failed'
+[ -L "$root/etc/runlevels/default/sddm" ] ||
+	fail 'OpenRC sddm was not linked into runlevels/default'
+
+present=$(setup_service_enable_present "$root" systemd) ||
+	fail 'enable_present failed'
+case $present in
+	*sddm*) ;;
+	*) fail "enable_present missed sddm: $present" ;;
+esac
+case $present in
+	*dbus*) ;;
+	*) fail "enable_present missed dbus: $present" ;;
+esac
+
+# Hooks no-op when init is none, and enable when init is recorded.
+printf '%s\n' none >"$root/etc/sps/init"
+rm -f "$root/etc/runlevels/default/sshd"
+export SPS_ROOT=$root SPS_PACKAGE=openssh
+setup_service_hook_enable || fail 'hook_enable with init none failed'
+
+printf '%s\n' openrc >"$root/etc/sps/init"
+printf '%s\n' '#!/bin/sh' >"$root/etc/init.d/sshd"
+chmod 755 "$root/etc/init.d/sshd"
+setup_service_hook_enable || fail 'hook_enable with openrc failed'
+[ -L "$root/etc/runlevels/default/sshd" ] ||
+	fail 'openssh hook did not enable sshd for OpenRC'
+
+if grep -q '^package[[:space:]]*elogind' "$project_dir/lib/setup/sets/power.set"
+then
+	fail 'power set must not pull elogind (conflicts with systemd)'
+fi
+
+printf '%s\n' 'service tests passed'
diff --git a/tests/test_setup.sh b/tests/test_setup.sh
index 95c469d..2c75424 100755
--- a/tests/test_setup.sh
+++ b/tests/test_setup.sh
@@ -148,6 +148,9 @@ grep -q '^tester:x:1000:1000:Test User:/home/tester:/bin/sh$' \
 [ -f "$root/root/sps-bootloader.txt" ] || fail 'bootloader notes missing'
 [ -f "$root/root/sps-network.txt" ] || fail 'network notes missing'
 [ -f "$root/root/sps-fstab.txt" ] || fail 'fstab notes missing'
+[ -f "$root/root/sps-services.txt" ] || fail 'services notes missing'
+grep -q 'Init is none' "$root/root/sps-services.txt" ||
+	fail 'fixture with init none should not pretend to enable services'
 grep -q 'grub-install' "$root/root/sps-bootloader.txt" ||
 	fail 'bootloader notes omitted grub-install warning'
 
@@ -226,3 +229,69 @@ grep -q 'git core https://github.com/RobertFlexx/sps-core.git' \
 grep -q 'git extra https://github.com/RobertFlexx/sps-extra.git' \
 	"$root_git/etc/sps/repos.conf" ||
 	fail 'empty live root should write git extra'
+
+# Later plans use the real profiles, not the fixture directory.
+unset SPS_SETUP_DIR
+
+# Guided layouts: --plan does not need --format yes; a real run does.
+plan=$("$setup" --plan --target /mnt --profile minimal --disable qol-cli \
+	--partition efi-root --disk /dev/vda)
+contains "$plan" 'Partition:  efi-root'
+contains "$plan" 'Format:     no'
+contains "$plan" 'Disk:       /dev/vda'
+
+plan=$("$setup" --plan --target /mnt --profile minimal --disable qol-cli \
+	--partition efi-root --disk /dev/vda --format yes)
+contains "$plan" 'Format:     yes'
+
+set +e
+"$setup" --plan --target /mnt --profile minimal --disable qol-cli \
+	--install-bootloader >/dev/null 2>"$tmp/err"
+st=$?
+set -e
+[ "$st" -eq 2 ] || fail "--install-bootloader without --disk returned $st"
+contains "$(cat "$tmp/err")" '--disk'
+
+set +e
+"$setup" --non-interactive --no-update --target "$tmp/noformat" \
+	--profile minimal --disable qol-cli --partition efi-root --disk /dev/vda \
+	>/dev/null 2>"$tmp/err"
+st=$?
+set -e
+[ "$st" -eq 2 ] || fail "guided without --format yes returned $st"
+contains "$(cat "$tmp/err")" '--format yes'
+
+plan=$("$setup" --plan --target /mnt --profile plasma-desktop --init systemd \
+	--disable qol-cli --disable power --disable firmware)
+contains "$plan" 'Init:       systemd'
+contains "$plan" 'sddm'
+contains "$plan" 'init-systemd'
+contains "$plan" 'Services:   yes'
+case $plan in
+	*elogind*) fail 'plasma + systemd must not pull elogind' ;;
+esac
+
+plan=$("$setup" --plan --target /mnt --profile plasma-desktop --init openrc \
+	--disable qol-cli)
+contains "$plan" 'init-openrc'
+
+set +e
+"$setup" --plan --target /mnt --profile minimal --init systemd \
+	--disable qol-cli --extra elogind >/dev/null 2>"$tmp/err"
+st=$?
+set -e
+[ "$st" -eq 7 ] || fail "systemd + extra elogind returned $st"
+contains "$(cat "$tmp/err")" 'elogind'
+
+plan=$("$setup" --plan --target /mnt --profile minimal --init systemd \
+	--disable qol-cli --extra sddm --services no)
+contains "$plan" 'sddm'
+contains "$plan" 'Services:   no'
+
+"$setup" --plan --target /mnt --profile minimal --disable qol-cli \
+	--write-answers "$tmp/svc-answers" >/dev/null
+grep -qx 'services yes' "$tmp/svc-answers" || fail 'answers missing services'
+grep -qx 'install-bootloader no' "$tmp/svc-answers" ||
+	fail 'answers missing install-bootloader'
+
+printf '%s\n' 'setup tests passed'