SPS · log · files · refs · tags · releases · people · languages · GitHub · commit
e045da0
setup: confirm guided format and enable init-aware services
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'