Splux Linux

source-based Linux using SPS

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

243f4d5


setup: install Splux without killing live init

Date
Commit243f4d52dfc6158bbf79a9e96c8179310f3b0453
GitHubmirror

Parents

Stat

 DESIGN.md                                 |  18 +-
 MANIFESTO.md                              |   9 +-
 Makefile                                  |  58 +++-
 PACKAGE-MAINTAINER.md                     |  13 +-
 README.md                                 |  69 +++-
 bin/mkiso                                 | 244 ++++++++++++--
 bin/mkpkg                                 |   4 +-
 bin/setup                                 | 535 ++++++++++++++++++++++++++++--
 bin/sget                                  |  16 +
 lib/deps.awk                              |  42 ++-
 lib/index.awk                             |   8 +-
 lib/mkiso/initrd-init                     | 106 +++---
 lib/mkiso/inittab                         |  12 +
 lib/mkiso/inittab-plasma                  |   8 +
 lib/mkiso/issue                           |   2 +-
 lib/mkiso/live-init                       |  83 ++---
 lib/mkiso/live-plasma                     |  38 +++
 lib/mkiso/live-rc                         |  90 +++++
 lib/mkiso/motd                            |  29 +-
 lib/mkiso/plasma-bins                     |  33 ++
 lib/mkiso/plasma-motd                     |  20 ++
 lib/mkiso/plasma-trees                    |  39 +++
 lib/mkiso/seed-bins                       |   4 +
 lib/mkiso/systemd/sps-live-plasma.service |  18 +
 lib/mkiso/systemd/sps-live-rc.service     |  15 +
 lib/mkiso/systemd/sps-live-serial.service |  19 ++
 lib/recipe.awk                            |   8 +-
 lib/relations.awk                         |   2 +-
 lib/repository.awk                        |   3 +-
 lib/setup/extras                          |  18 +
 lib/setup/hwdetect.sh                     | 103 ++++++
 lib/setup/partition.sh                    | 282 ++++++++++++++++
 lib/setup/sets/drivers.set                |   9 +
 lib/setup/sets/firmware.set               |   2 +-
 man/mkiso.1                               |  33 +-
 man/setup.1                               |  33 +-
 tests/run                                 |   6 +
 tests/test_conflicts.sh                   |  66 ++++
 tests/test_install.sh                     |   5 +-
 tests/test_mkiso.sh                       |  19 ++
 tests/test_recipe_contract.sh             |   2 +-
 tests/test_repository.sh                  |   2 +-
 tests/test_setup.sh                       |  12 +-
 43 files changed, 1907 insertions(+), 230 deletions(-)

Diff

diff --git a/DESIGN.md b/DESIGN.md
index 560b6d0..f9e92b5 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -1,8 +1,10 @@
 # SPS 1.0 formats and behavior
 
 This file documents the interfaces that packages and distro tooling can depend
-on in SPS Package metadata,
-indexes, and installed records are meant to be inspectable and scriptable.
+on in SPS 1.0.
+
+Package metadata, indexes, and installed records are meant to be inspectable
+and scriptable.
 
 The current package format is `format 1`. If a later SPS release needs an
 incompatible package layout, it must use a new format number.
@@ -33,8 +35,15 @@ hash
 depend
 builddep
 optional
+conflict
 ```
 
+`conflict` names packages that must not be installed together. `sget install`
+refuses a transaction that contains both a package and one of its conflicts,
+and also refuses to install a package that conflicts with something already
+in the database. `src update` still validates the whole graph; two packages
+that conflict may both exist in the repository.
+
 `description` is a single-value field.
 
 Build phases are repeatable command fields:
@@ -84,9 +93,12 @@ Indexes live under `$SPS_CACHE/indexes`. Each package record is tab-separated
 and contains:
 
 ```text
-name version release arch depends builddeps optional repo priority recipe_path description definition_sha256
+name version release arch depends builddeps optional repo priority recipe_path description definition_sha256 conflicts
 ```
 
+`conflicts` is empty when the recipe has no `conflict` records. Older 12-field
+index files are still accepted; missing conflicts are treated as none.
+
 Dependency lists are comma-separated. Field values may not contain tabs or
 newlines. The definition digest covers the recipe content and the relative
 paths, contents, and copied modes of its `files/`, `patches/`, and `hooks/`
diff --git a/MANIFESTO.md b/MANIFESTO.md
index 23e1cd7..61b9132 100644
--- a/MANIFESTO.md
+++ b/MANIFESTO.md
@@ -4,7 +4,10 @@
 
 SPS is a source package system for Unix-like operating systems. It is meant for
 small independent distributions and for administrators who want to understand
-how packages get from a recipe into the filesystem.
+how packages get from a recipe into the filesystem. SPS is not itself a
+distribution. **SPS Linux** (informally Splux) is one distro that uses it;
+Linux From Scratch and other trees can use the same tools without the Splux
+installer or live ISOs.
 
 It is built from a few separate tools, plain text metadata, POSIX shell, POSIX
 AWK, and standard Unix utilities. There is no package daemon, SQL database,
@@ -34,8 +37,8 @@ pkdel     low-level package removal
 pkstat    installed package queries
 pkcheck   installed-state verification
 pkmark    explicit/dependency package marking
-setup     dialog-based system installer
-mkiso     live TTY ISO builder
+setup     SPS Linux (Splux) dialog installer; optional, distro-specific
+mkiso     SPS Linux live ISO builder; optional, distro-specific
 ```
 
 Each tool has a boundary.
diff --git a/Makefile b/Makefile
index 58d79f1..9e1785a 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,7 @@ PROGRAMS = src sget mkpkg pkin pkdel pkstat pkcheck pkmark setup mkiso
 AWK_LIBS = common.awk config.awk db.awk deps.awk index.awk package.awk recipe.awk repository.awk relations.awk version.awk
 
 .PHONY: all check lint check-version source-archive release-check \
-	install install-config clean
+	install install-config clean iso iso-slim iso-plasma
 
 all:
 	@printf '%s\n' "SPS is interpreted; run 'make check' or 'make install'."
@@ -25,6 +25,15 @@ lint:
 	@set -e; for library in lib/*.sh; do \
 		$(POSIX_SHELL) -n $$library; \
 	done
+	@set -e; for library in lib/setup/*.sh; do \
+		[ -f $$library ] || continue; \
+		$(POSIX_SHELL) -n $$library; \
+	done
+	@set -e; for script in lib/mkiso/initrd-init lib/mkiso/live-rc \
+		lib/mkiso/live-plasma lib/mkiso/live-init; do \
+		[ -f $$script ] || continue; \
+		$(POSIX_SHELL) -n $$script; \
+	done
 	@# Pure library modules can be parsed without an action or fixture.  The
 	@# executable AWK modules validate their input in END rules, so the test
 	@# suite below is their syntax + behavior check rather than invoking them
@@ -163,12 +172,22 @@ install:
 	@set -e; for f in lib/mkiso/*; do \
 		[ -f "$$f" ] || continue; \
 		case $$f in \
-			*/live-init|*/initrd-init) \
+			*/live-init|*/initrd-init|*/live-rc|*/live-plasma) \
 				install -m 0755 $$f "$(DESTDIR)$(LIBDIR)/mkiso/" ;; \
 			*) \
 				install -m 0644 $$f "$(DESTDIR)$(LIBDIR)/mkiso/" ;; \
 		esac; \
 	done
+	install -d "$(DESTDIR)$(LIBDIR)/mkiso/systemd"
+	@set -e; for f in lib/mkiso/systemd/*; do \
+		[ -f "$$f" ] || continue; \
+		install -m 0644 $$f "$(DESTDIR)$(LIBDIR)/mkiso/systemd/"; \
+	done
+	install -d "$(DESTDIR)$(LIBDIR)/setup"
+	@set -e; for f in lib/setup/*.sh; do \
+		[ -f "$$f" ] || continue; \
+		install -m 0644 $$f "$(DESTDIR)$(LIBDIR)/setup/"; \
+	done
 
 # Configuration is separate so installation never overwrites administrator
 # policy. Existing files are deliberately left untouched.
@@ -179,6 +198,41 @@ install-config:
 	@test -e "$(DESTDIR)$(SYSCONFDIR)/repos.conf" || \
 		install -m 0644 examples/repos.conf "$(DESTDIR)$(SYSCONFDIR)/repos.conf"
 
+# SPS Linux (Splux) live images. busybox must be a static binary; the host
+# copy is usually dynamically linked and will not survive in an initramfs.
+ISO_BUSYBOX ?= /tmp/sps-wave/busybox.static
+ISO_CORE ?= /usr/src/sps/core
+ISO_EXTRA ?= /usr/src/sps/extra
+ISO_DIR ?= dist
+
+iso:
+	@test -x "$(ISO_BUSYBOX)" || { \
+		printf '%s\n' "iso: static busybox not found: $(ISO_BUSYBOX)" >&2; \
+		exit 1; \
+	}
+	mkdir -p "$(ISO_DIR)"
+	bin/mkiso --output "$(ISO_DIR)/sps-live.iso" --init systemd \
+		--busybox "$(ISO_BUSYBOX)" --core "$(ISO_CORE)" --extra "$(ISO_EXTRA)"
+
+iso-slim:
+	@test -x "$(ISO_BUSYBOX)" || { \
+		printf '%s\n' "iso-slim: static busybox not found: $(ISO_BUSYBOX)" >&2; \
+		exit 1; \
+	}
+	mkdir -p "$(ISO_DIR)"
+	bin/mkiso --output "$(ISO_DIR)/sps-live-slim.iso" --init systemd \
+		--no-seed --no-modules --busybox "$(ISO_BUSYBOX)"
+
+iso-plasma:
+	@test -x "$(ISO_BUSYBOX)" || { \
+		printf '%s\n' "iso-plasma: static busybox not found: $(ISO_BUSYBOX)" >&2; \
+		exit 1; \
+	}
+	mkdir -p "$(ISO_DIR)"
+	bin/mkiso --output "$(ISO_DIR)/sps-live-plasma.iso" \
+		--session plasma --init systemd \
+		--busybox "$(ISO_BUSYBOX)" --core "$(ISO_CORE)" --extra "$(ISO_EXTRA)"
+
 clean:
 	@find . -type f \( -name '*.pkg.tar' -o -name '*.pkg.tar.gz' -o \
 		-name '*.pkg.tar.xz' -o -name '*.pkg.tar.zst' \) -exec rm -f {} \;
diff --git a/PACKAGE-MAINTAINER.md b/PACKAGE-MAINTAINER.md
index e826418..4bbd677 100644
--- a/PACKAGE-MAINTAINER.md
+++ b/PACKAGE-MAINTAINER.md
@@ -4,6 +4,10 @@ An SPS package directory contains a line-oriented file named `recipe`. It may
 also contain `files/`, `patches/`, and `hooks/`. Recipes are data, not shell
 files: repository indexing parses them without executing build commands.
 
+The same recipe format works anywhere SPS runs, including Linux From Scratch
+and custom roots. `linux-desktop`, Plasma profiles, and `setup` defaults are
+**SPS Linux** (Splux) distro pieces; an LFS project can ignore them.
+
 ## Recipe records
 
 Each logical line is a key, whitespace, and a non-empty value. Blank lines and
@@ -24,15 +28,20 @@ The complete key set is:
 | `depend` | repeatable | Runtime dependency |
 | `builddep` | repeatable | Build dependency |
 | `optional` | repeatable | Informational optional dependency |
+| `conflict` | repeatable | Package names that cannot be co-installed |
 | `prepare` | repeatable | Source preparation command |
 | `configure` | repeatable | Configuration command |
 | `build` | repeatable | Build command |
 | `install` | one or more | Staged installation command |
 
 Unknown keys are errors. SPS 1.0 has no recipe keys for homepage, license,
-maintainer, conflicts, replacements, provides, or package options. Keep that
+maintainer, replacements, provides, or package options. Keep that
 information in repository documentation when needed; do not invent fields.
 
+`conflict` is supported: `sget` will not install a package next to a named
+conflict. Use it for init systems (`systemd` vs `openrc`) and for udev
+implementations (`systemd` vs `eudev`).
+
 `name`, `version`, `release`, and at least one `install` record are required.
 The install phase must produce a non-empty package. `name` must begin with an
 alphanumeric character and may then contain letters, digits, `+`, `_`, `.`, or
@@ -71,7 +80,7 @@ not independently validate every tar link target before extraction.
 
 ## Dependencies
 
-Use the singular keys `depend`, `builddep`, and `optional`. A record may contain
+Use the singular keys `depend`, `builddep`, `optional`, and `conflict`. A record may contain
 one or more whitespace- or comma-separated specifications. Version constraints
 have no spaces around the operator:
 
diff --git a/README.md b/README.md
index 9ce792e..13c762c 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,26 @@ If you REALLY want the project rationale and the boundaries of the design, read
 [MANIFESTO.md](MANIFESTO.md). The exact 1.0 file formats and behavior are in
 [DESIGN.md](DESIGN.md).
 
+## SPS is not the distro
+
+SPS is the source package system. It is not locked to one operating system.
+You can run `src`, `sget`, `mkpkg`, `pkin`, and the rest on Linux From Scratch,
+on a custom chroot, or next to another distro's userland. Recipes are plain
+text; an LFS tree can consume sps-core and sps-extra the same way SPS Linux
+does.
+
+**SPS Linux** is a Linux distribution that *uses* SPS. Informally it is called
+**Splux**. Official installer flow, live ISOs, Plasma profiles, and the
+`linux-desktop` kernel are SPS Linux pieces. They live in this tree and in
+the core/extra collections because that is how Splux is built and installed.
+
+If you take SPS into an LFS (or any other) project, do not assume `setup`
+will do the right thing for you. The installer partitions disks, writes
+fstab, chooses systemd or OpenRC, copies a Splux hostname, and installs
+distro profiles. Your LFS book already has its own disk, boot, and
+configuration story. Use `sget` and `mkpkg` there; skip `setup` and `mkiso`
+unless you actually want SPS Linux on that machine.
+
 ## Tools
 
 SPS is a set of tools rather than one large front end:
@@ -24,8 +44,10 @@ SPS is a set of tools rather than one large front end:
 - `pkstat` reads the installed package database.
 - `pkcheck` checks installed files and database consistency.
 - `pkmark` changes whether an installed package is explicit or dependency-only.
-- `setup` is a dialog(1) system installer (Slackware/FreeBSD style).
-- `mkiso` builds a live TTY ISO you can flash and boot into `setup`.
+- `setup` is the SPS Linux (Splux) dialog(1) installer (Slackware/FreeBSD
+  style). Optional; other projects using SPS do not need it.
+- `mkiso` builds an SPS Linux live ISO (systemd by default; busybox init if
+  systemd is not on the build host) you can flash and boot into `setup`.
 
 The split matters. `pkin` does not search repositories. `pkdel` does not solve
 dependencies. `src` does not install anything. `sget` ties the lower-level tools
@@ -138,10 +160,10 @@ pkdel foo
 This is useful for recovery work, distro installers, package development, and
 cases where the administrator wants to bypass dependency policy on purpose.
 
-## Install a system
+## Install SPS Linux (Splux)
 
-`setup` is the distro installer. It uses `dialog` screens, or the same choices
-from flags for scripts:
+`setup` is the **SPS Linux** installer, not a generic SPS or LFS installer.
+It uses `dialog` screens, or the same choices from flags for scripts:
 
 ```sh
 setup
@@ -156,33 +178,46 @@ SSH, and more. `setup --list-sets` prints them. There is also an extra-package
 checklist (`--extra`) plus locale and login-shell screens.
 
 The installer writes hostname, timezone, keymap, and a first user into the
-target, then runs `src update` and `sget install`. It does not partition disks
-and does not run `grub-install` unless you pass `--install-bootloader`, which
-still refuses to guess a disk.
+target, then runs `src update` and `sget install`. Guided partitioning is
+optional and waits until the last confirmation. Choose systemd or OpenRC
+as PID 1 (`--init`); those packages conflict. `grub-install` still needs
+`--install-bootloader` and `--disk`.
 
-## Live TTY ISO
+## SPS Linux live ISO
 
-`mkiso` writes a hybrid ISO. Flash it to USB, boot to a console, mount a
-destination filesystem on `/mnt`, and run `setup`. Ready-made images:
+`mkiso` writes a hybrid ISO for SPS Linux (Splux). Flash it to USB, boot to a
+console, mount a destination filesystem on `/mnt`, and run `setup`. Ready-made
+images:
 
-https://github.com/RobertFlexx/SPS/releases/tag/live-2026-08-31
+https://github.com/RobertFlexx/SPS/releases/tag/live-2026-08-31-2
 
 ```sh
-mkiso --output dist/sps-live.iso \
+mkiso --output dist/sps-live.iso --init systemd \
   --busybox /path/to/busybox \
   --core /usr/src/sps/core \
   --extra /usr/src/sps/extra
+
+make iso
+make iso-slim
+make iso-plasma
 ```
 
+Live PID 1 is systemd when the build host has it, otherwise busybox init
+with gettys on tty1 and ttyS0. The earlier live TTY image exec'd a shell
+as PID 1 and panicked in QEMU (`Attempted to kill init`, exit 127) when
+that shell exited. `--session plasma` copies a host Plasma/Wayland stack;
+NVIDIA live graphics go through nouveau.
+
 `--layout DIR` writes the live root without making an ISO. `--no-seed` skips
 copying the host compiler. `--with-firmware` copies `/lib/firmware` (large;
 published images omit it). The live image never runs `grub-install` on a disk.
 
 Download a ready-made hybrid ISO from this repository's GitHub Releases, flash
-it to USB, boot the "SPS live TTY" entry, mount a destination filesystem on
-`/mnt`, and run `setup`. The image includes the installer, sps-core and
-sps-extra recipes, and a host seed compiler so `setup` can build packages.
-A full Plasma install compiles Qt and KDE from source and takes a long time.
+it to USB, boot the "SPS Linux live (Splux)" entry, mount a destination
+filesystem on `/mnt`, and run `setup`. The image includes the installer,
+sps-core and sps-extra recipes, and a host seed compiler so `setup` can build
+packages. A full Plasma install compiles Qt and KDE from source and takes a
+long time.
 
 ```sh
 # flash (example)
diff --git a/bin/mkiso b/bin/mkiso
index aa0f479..5c41d50 100755
--- a/bin/mkiso
+++ b/bin/mkiso
@@ -1,7 +1,33 @@
 #!/bin/sh
-# Build a bootable SPS live TTY ISO. Users flash it, boot to a console, mount
-# a destination filesystem, and run setup(1). It never runs grub-install on a
-# disk.
+# mkiso builds SPS Linux live ISOs (informally Splux). Maintainer tool;
+# not a desktop installer front-end. SPS the package system does not
+# require these images; they exist so Splux can be installed from USB.
+#
+# Default image: hybrid ISO, setup(1) on PATH, gettys on tty1 and ttyS0.
+# Flash it, boot it, run setup. Guided partitioning is in setup, not here.
+#
+# --session plasma copies Plasma/Wayland from this host. Qt is not compiled
+# during the ISO build. If startplasma-wayland is missing, the disc still
+# boots to a TTY. NVIDIA on the live image is nouveau; proprietary nvidia*
+# modules are blacklisted so they cannot load next to KWin. From Plasma,
+# open Konsole and run setup.
+#
+# --with-firmware copies all of /lib/firmware (often over a gigabyte).
+# Plasma sessions also copy nvidia/nouveau firmware without that flag;
+# TU-class cards need those blobs for nouveau.
+#
+# BusyBox --install -s writes host-absolute applet links. Those break
+# inside the initramfs. mkiso uses relative `ln -sf busybox applet`.
+#
+# grub-install is never run against a disk. The GRUB payload is El Torito
+# plus an ESP for this ISO only. grub-mkrescue wants unicode.pf2, which
+# this host does not ship, so the image is built with grub-mkimage and
+# xorriso.
+#
+# Seed toolchain (--no-seed to skip): gcc, headers, cmake, ninja, python.
+# Pass --core/--extra so the live image can build packages without cloning
+# the recipe repos. --with-go copies the official Go tree; gccgo on PATH
+# is too old for the Go recipes in extra.
 
 sps_prog=mkiso
 sps_bin_dir=$(CDPATH= cd -P "$(dirname "$0")" 2>/dev/null && pwd) || exit 1
@@ -23,12 +49,20 @@ mkiso_usage()
 usage: mkiso --output FILE.iso [options]
        mkiso --layout DIR [options]
 
-Build a live TTY ISO (or a layout directory) that boots to a console with
-setup(1) on PATH.
+Build a live ISO (or a layout directory) for SPS Linux, informally Splux.
+Flash it, boot it, run setup(1) on a spare disk.
+
+Sessions:
+  tty      root shell on tty1 and ttyS0 (default; always works)
+  plasma   copy host Plasma/Wayland into the squashfs; Konsole -> setup
+           NVIDIA live path is nouveau. Falls back to a TTY if Plasma
+           is missing on the build host.
 
 options:
   --output FILE         write a hybrid ISO
   --layout DIR          populate a live root and stop (tests, inspection)
+  --session NAME        tty or plasma (default: tty)
+  --init NAME           systemd or openrc (default: systemd)
   --work DIR            working directory (default: a temp dir)
   --kernel FILE         vmlinuz (default: /boot/vmlinuz)
   --modules DIR         kernel modules (default: /lib/modules/RELEASE)
@@ -36,11 +70,13 @@ options:
   --core DIR            sps-core recipes (copied to /usr/src/sps/core)
   --extra DIR           sps-extra recipes (copied to /usr/src/sps/extra)
   --with-rust           copy rustc and cargo from the host
-  --with-go             copy the host Go toolchain
-  --with-firmware       copy /lib/firmware (large; omit from published ISOs)
+  --with-go             copy the host Go toolchain (not gccgo)
+  --with-firmware       copy all of /lib/firmware (large)
   --no-modules          do not copy /lib/modules (smaller, less hardware)
   --no-seed             do not copy host compiler tools (layout/tests)
   -h, --help
+
+Makefile wrappers: make iso, make iso-slim, make iso-plasma.
 EOF
 }
 
@@ -58,6 +94,8 @@ mkiso_with_go=0
 mkiso_with_firmware=0
 mkiso_copy_modules=1
 mkiso_copy_seed=1
+mkiso_session=tty
+mkiso_init=systemd
 mkiso_libdir=$SPS_LIBDIR/mkiso
 
 mkiso_need_value()
@@ -117,6 +155,16 @@ while [ "$#" -gt 0 ]; do
 		--with-firmware) mkiso_with_firmware=1; shift ;;
 		--no-modules) mkiso_copy_modules=0; shift ;;
 		--no-seed) mkiso_copy_seed=0; shift ;;
+		--session)
+			mkiso_need_value "$@"
+			mkiso_session=$2
+			shift 2
+			;;
+		--init)
+			mkiso_need_value "$@"
+			mkiso_init=$2
+			shift 2
+			;;
 		-*)
 			sps_die "$SPS_EX_USAGE" "unknown option: $1"
 			;;
@@ -129,6 +177,14 @@ done
 if [ -z "$mkiso_output" ] && [ -z "$mkiso_layout" ]; then
 	sps_die "$SPS_EX_USAGE" "pass --output FILE.iso or --layout DIR"
 fi
+case $mkiso_session in
+	tty|plasma) ;;
+	*) sps_die "$SPS_EX_USAGE" "session must be tty or plasma" ;;
+esac
+case $mkiso_init in
+	systemd|openrc) ;;
+	*) sps_die "$SPS_EX_USAGE" "init must be systemd or openrc" ;;
+esac
 
 mkiso_cleanup()
 {
@@ -228,6 +284,148 @@ mkiso_busybox_links()
 	ln -sf busybox "$mkiso_bbdir/sh"
 }
 
+# Copy a host Plasma/Wayland stack. mkiso does not compile Qt.
+mkiso_copy_plasma()
+{
+	printf '%s: copying host Plasma/Wayland into the live root\n' \
+		"$sps_prog" >&2
+	if [ -f "$mkiso_libdir/plasma-bins" ]; then
+		while IFS= read -r mkiso_bin || [ -n "$mkiso_bin" ]; do
+			case $mkiso_bin in ''|\#*) continue ;; esac
+			[ -e "$mkiso_bin" ] || continue
+			mkiso_copy_path "$mkiso_bin"
+			mkiso_copy_libs "$mkiso_bin"
+		done <"$mkiso_libdir/plasma-bins"
+	fi
+	if [ -f "$mkiso_libdir/plasma-trees" ]; then
+		while IFS= read -r mkiso_tree || [ -n "$mkiso_tree" ]; do
+			case $mkiso_tree in ''|\#*) continue ;; esac
+			[ -e "$mkiso_tree" ] || continue
+			mkiso_copy_path "$mkiso_tree"
+		done <"$mkiso_libdir/plasma-trees"
+	fi
+	# Shared objects the session links by soname, not by a binary's ldd.
+	for mkiso_pat in /usr/lib64/libQt6*.so* /usr/lib64/libKF6*.so* \
+		/usr/lib64/libplasma*.so* /usr/lib64/libkwin*.so* \
+		/usr/lib64/libwayland*.so* /usr/lib64/libGLESv2.so* \
+		/usr/lib64/libEGL.so* /usr/lib64/libGL.so*
+	do
+		for mkiso_so in $mkiso_pat; do
+			[ -e "$mkiso_so" ] || continue
+			mkiso_copy_path "$mkiso_so"
+		done
+	done
+}
+
+# PID 1 must be an ELF. A shell script that execs getty/setsid is what
+# panicked the previous live TTY (init exited 127).
+mkiso_install_pid1()
+{
+	mkdir -p "$mkiso_live/sbin" "$mkiso_live/etc/sps" \
+		"$mkiso_live/etc/systemd/system" \
+		"$mkiso_live/etc/runlevels/default" \
+		"$mkiso_live/etc/runlevels/sysinit" \
+		"$mkiso_live/etc/modprobe.d"
+	install -m 0755 "$mkiso_libdir/live-rc" "$mkiso_live/etc/sps/live-rc"
+	install -m 0755 "$mkiso_libdir/live-plasma" "$mkiso_live/etc/sps/live-plasma"
+	printf '%s\n' "$mkiso_init" >"$mkiso_live/etc/sps/init"
+	printf '%s\n' "$mkiso_session" >"$mkiso_live/etc/sps/live-session"
+
+	mkiso_systemd=
+	for mkiso_p in /usr/lib/systemd/systemd /lib/systemd/systemd \
+		/usr/lib64/systemd/systemd "$mkiso_live/usr/lib/systemd/systemd"
+	do
+		if [ -x "$mkiso_p" ]; then
+			mkiso_systemd=$mkiso_p
+			break
+		fi
+	done
+
+	mkiso_openrc=
+	for mkiso_p in /sbin/openrc-init /usr/sbin/openrc-init \
+		"$mkiso_live/sbin/openrc-init"
+	do
+		if [ -x "$mkiso_p" ]; then
+			mkiso_openrc=$mkiso_p
+			break
+		fi
+	done
+
+	# Always plant busybox init + inittab so the disc boots even when
+	# the preferred init is not on the build host.
+	if [ -n "$mkiso_busybox" ] && [ -x "$mkiso_busybox" ]; then
+		cp -a "$mkiso_busybox" "$mkiso_live/bin/busybox"
+		chmod 755 "$mkiso_live/bin/busybox"
+		mkiso_busybox_links "$mkiso_live/bin"
+		if [ -x "$mkiso_live/bin/bash" ]; then
+			ln -sf bash "$mkiso_live/bin/sh"
+		fi
+		cp -a "$mkiso_live/bin/busybox" "$mkiso_live/sbin/init"
+		chmod 755 "$mkiso_live/sbin/init"
+	fi
+	if [ "$mkiso_session" = plasma ]; then
+		install -m 0644 "$mkiso_libdir/inittab-plasma" \
+			"$mkiso_live/etc/inittab"
+	else
+		install -m 0644 "$mkiso_libdir/inittab" "$mkiso_live/etc/inittab"
+	fi
+
+	if [ "$mkiso_init" = systemd ] && [ -n "$mkiso_systemd" ]; then
+		printf '%s: live PID 1 is systemd (%s)\n' \
+			"$sps_prog" "$mkiso_systemd" >&2
+		mkiso_copy_path "$mkiso_systemd"
+		mkiso_copy_libs "$mkiso_systemd"
+		mkiso_copy_path /usr/bin/systemctl
+		mkiso_copy_libs /usr/bin/systemctl
+		mkiso_copy_path /usr/lib/systemd
+		mkiso_copy_path /lib/systemd
+		mkdir -p "$mkiso_live/etc/systemd/system/multi-user.target.wants" \
+			"$mkiso_live/etc/systemd/system/graphical.target.wants" \
+			"$mkiso_live/etc/systemd/system/getty.target.wants"
+		install -m 0644 "$mkiso_libdir/systemd/sps-live-rc.service" \
+			"$mkiso_live/etc/systemd/system/"
+		install -m 0644 "$mkiso_libdir/systemd/sps-live-serial.service" \
+			"$mkiso_live/etc/systemd/system/"
+		ln -sf /etc/systemd/system/sps-live-rc.service \
+			"$mkiso_live/etc/systemd/system/multi-user.target.wants/"
+		ln -sf /etc/systemd/system/sps-live-serial.service \
+			"$mkiso_live/etc/systemd/system/multi-user.target.wants/"
+		if [ "$mkiso_session" = plasma ]; then
+			install -m 0644 "$mkiso_libdir/systemd/sps-live-plasma.service" \
+				"$mkiso_live/etc/systemd/system/"
+			ln -sf /etc/systemd/system/sps-live-plasma.service \
+				"$mkiso_live/etc/systemd/system/graphical.target.wants/"
+			ln -sf /usr/lib/systemd/system/graphical.target \
+				"$mkiso_live/etc/systemd/system/default.target"
+		else
+			ln -sf /usr/lib/systemd/system/multi-user.target \
+				"$mkiso_live/etc/systemd/system/default.target"
+			ln -sf /usr/lib/systemd/system/getty@.service \
+				"$mkiso_live/etc/systemd/system/getty.target.wants/getty@tty1.service"
+		fi
+		# systemd as PID 1: kernel execs this ELF, not a shell.
+		mkiso_sys_dest=$mkiso_live$mkiso_systemd
+		if [ -x "$mkiso_sys_dest" ]; then
+			ln -sf "$mkiso_systemd" "$mkiso_live/sbin/init"
+		fi
+	elif [ "$mkiso_init" = openrc ] && [ -n "$mkiso_openrc" ]; then
+		printf '%s: live PID 1 is OpenRC (%s)\n' \
+			"$sps_prog" "$mkiso_openrc" >&2
+		mkiso_copy_path "$mkiso_openrc"
+		mkiso_copy_libs "$mkiso_openrc"
+		ln -sf "$mkiso_openrc" "$mkiso_live/sbin/init"
+	else
+		printf '%s: live PID 1 is busybox init (host has no %s binaries)\n' \
+			"$sps_prog" "$mkiso_init" >&2
+	fi
+
+	if [ ! -x "$mkiso_live/sbin/init" ]; then
+		install -m 0755 "$mkiso_libdir/live-init" "$mkiso_live/sbin/init"
+	fi
+	[ -x "$mkiso_live/sbin/init" ] ||
+		sps_die "$SPS_EX_PACKAGE" "failed to install /sbin/init"
+}
+
 # --- live root contents -------------------------------------------------
 
 printf '%s\n' "mkiso: installing SPS tools into the live root" >&2
@@ -267,11 +465,10 @@ if [ -n "$mkiso_busybox" ] && [ -x "$mkiso_busybox" ]; then
 	ln -sf ../bin/busybox "$mkiso_live/sbin/busybox" 2>/dev/null || :
 fi
 
-install -m 0755 "$mkiso_libdir/live-init" "$mkiso_live/sbin/init"
 install -m 0644 "$mkiso_libdir/motd" "$mkiso_live/etc/motd"
 install -m 0644 "$mkiso_libdir/issue" "$mkiso_live/etc/issue"
 printf '%s\n' live >"$mkiso_live/etc/sps/live"
-printf '%s\n' sps-live >"$mkiso_live/etc/hostname"
+printf '%s\n' splux-live >"$mkiso_live/etc/hostname"
 printf '%s\n' 'root:x:0:0:root:/root:/bin/sh' >"$mkiso_live/etc/passwd"
 printf '%s\n' 'root:x:0:' >"$mkiso_live/etc/group"
 # Empty password on the live console only.
@@ -353,6 +550,13 @@ if [ "$mkiso_copy_seed" -eq 1 ] && [ -f "$mkiso_libdir/seed-bins" ]; then
 	fi
 fi
 
+if [ "$mkiso_session" = plasma ]; then
+	mkiso_copy_plasma
+	if [ -f "$mkiso_libdir/plasma-motd" ]; then
+		install -m 0644 "$mkiso_libdir/plasma-motd" "$mkiso_live/etc/motd"
+	fi
+fi
+
 # Initramfs modules are required to mount the ISO even when the live
 # squashfs does not ship the full /lib/modules tree.
 if [ -z "$mkiso_modules" ]; then
@@ -366,8 +570,8 @@ if [ "$mkiso_copy_modules" -eq 1 ] && [ -d "$mkiso_modules" ]; then
 	cp -a "$mkiso_modules" "$mkiso_live/lib/modules/"
 fi
 
-# Busybox getty/init must win over a dangling seed copy of /sbin/init.
-install -m 0755 "$mkiso_libdir/live-init" "$mkiso_live/sbin/init"
+# PID 1 last, after seed, so a copied /sbin/init cannot stick around.
+mkiso_install_pid1
 
 if [ -n "$mkiso_layout" ] && [ -z "$mkiso_output" ]; then
 	printf '%s\n' "$mkiso_live"
@@ -477,20 +681,22 @@ printf '%s: packing initramfs\n' "$sps_prog" >&2
 ) | gzip -n -9 >"$mkiso_iso/live/initrd.img" ||
 	sps_die "$SPS_EX_BUILD" "initramfs cpio failed"
 
-cat >"$mkiso_iso/boot/grub/grub.cfg" <<'EOF'
+mkiso_title="SPS Linux live (Splux)"
+[ "$mkiso_session" = plasma ] && mkiso_title="SPS Linux live Plasma (Splux)"
+cat >"$mkiso_iso/boot/grub/grub.cfg" <<EOF
 set timeout=8
 set default=0
 serial --unit=0 --speed=115200
 terminal_input serial console
 terminal_output serial console
 
-menuentry "SPS live TTY" {
-	linux /live/vmlinuz boot=live console=tty0 console=ttyS0,115200n8
+menuentry "$mkiso_title" {
+	linux /live/vmlinuz boot=live sps.init=$mkiso_init console=tty0 console=ttyS0,115200n8
 	initrd /live/initrd.img
 }
 
-menuentry "SPS live TTY (run setup)" {
-	linux /live/vmlinuz boot=live sps=setup console=tty0 console=ttyS0,115200n8
+menuentry "$mkiso_title (run setup)" {
+	linux /live/vmlinuz boot=live sps.init=$mkiso_init sps=setup console=tty0 console=ttyS0,115200n8
 	initrd /live/initrd.img
 }
 EOF
@@ -524,7 +730,7 @@ if [ -n "$mkiso_efi" ]; then
 		sps_die "$SPS_EX_BUILD" "grub-mkimage (x86_64-efi) failed"
 	truncate -s 8M "$mkiso_iso/boot/grub/efi.img"
 	if command -v mformat >/dev/null 2>&1 && command -v mcopy >/dev/null 2>&1; then
-		mformat -v SPS_EFI -i "$mkiso_iso/boot/grub/efi.img" ::
+		mformat -v SPLUX_EFI -i "$mkiso_iso/boot/grub/efi.img" ::
 		mmd -i "$mkiso_iso/boot/grub/efi.img" ::EFI ::EFI/BOOT
 		mcopy -i "$mkiso_iso/boot/grub/efi.img" \
 			"$mkiso_iso/EFI/BOOT/BOOTX64.EFI" ::EFI/BOOT/BOOTX64.EFI
@@ -545,7 +751,7 @@ esac
 mkdir -p "$(dirname "$mkiso_output")"
 mkiso_xorriso_log=$mkiso_work/xorriso.log
 if [ -f "$mkiso_iso/boot/grub/efi.img" ]; then
-	xorriso -as mkisofs -r -J -V SPS_LIVE \
+	xorriso -as mkisofs -r -J -V SPLUX_LIVE \
 		-o "$mkiso_output" \
 		-b boot/grub/bios.img -no-emul-boot -boot-load-size 4 \
 		-boot-info-table --grub2-boot-info \
@@ -555,7 +761,7 @@ if [ -f "$mkiso_iso/boot/grub/efi.img" ]; then
 		"$mkiso_iso" >"$mkiso_xorriso_log" 2>&1 ||
 		sps_die "$SPS_EX_BUILD" "xorriso failed (see $mkiso_xorriso_log)"
 else
-	xorriso -as mkisofs -r -J -V SPS_LIVE \
+	xorriso -as mkisofs -r -J -V SPLUX_LIVE \
 		-o "$mkiso_output" \
 		-b boot/grub/bios.img -no-emul-boot -boot-load-size 4 \
 		-boot-info-table --grub2-boot-info \
diff --git a/bin/mkpkg b/bin/mkpkg
index c35bdf3..53aa62e 100755
--- a/bin/mkpkg
+++ b/bin/mkpkg
@@ -274,7 +274,7 @@ function allowed(key) {
 	return key == "name" || key == "version" || key == "release" ||
 	       key == "arch" || key == "description" || key == "source" ||
 	       key == "hash" || key == "depend" || key == "builddep" ||
-	       key == "optional" || key == "prepare" || key == "configure" ||
+	       key == "optional" || key == "conflict" || key == "prepare" || key == "configure" ||
 	       key == "build" || key == "install"
 }
 function emit(text, n, key, value, at) {
@@ -797,7 +797,7 @@ $1 == "description" {
 	next
 }
 $1 == "source" || $1 == "hash" || $1 == "depend" ||
-$1 == "builddep" || $1 == "optional" {
+$1 == "builddep" || $1 == "optional" || $1 == "conflict" {
 	repeated[++count] = $0
 }
 END {
diff --git a/bin/setup b/bin/setup
index e77ba47..ea6f2b8 100755
--- a/bin/setup
+++ b/bin/setup
@@ -1,6 +1,8 @@
 #!/bin/sh
-# SPS system installer. Interactive screens use dialog(1); every choice is
-# also available as a command-line option for scripts and tests.
+# 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
@@ -22,8 +24,14 @@ setup_usage()
 	cat <<'EOF'
 usage: setup [options]
 
-Install an SPS system into a target root using dialog(1) screens, or the
-same choices from the command line.
+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)
@@ -41,9 +49,23 @@ options:
   --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           include the nvidia set
+  --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
+  --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
-  --install-bootloader      actually run grub-install (explicit, dangerous)
+  --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
@@ -51,11 +73,20 @@ options:
   --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
 setup_work=
 setup_interactive=1
 setup_plan=0
@@ -82,6 +113,20 @@ 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_part_efi=
+setup_part_root=
+setup_part_swap=
+setup_part_home=
+setup_part_bios=
+setup_list_hardware=0
 
 setup_cleanup()
 {
@@ -209,6 +254,70 @@ while [ "$#" -gt 0 ]; do
 			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
+			;;
+		--list-hardware)
+			setup_list_hardware=1
+			shift
+			;;
 		--bootloader)
 			setup_need_value "$@"
 			setup_bootloader=$2
@@ -345,8 +454,9 @@ setup_default_sets_for_profile()
 				setup_ds="$setup_ds $setup_sn"
 				;;
 			*)
-				# Power defaults on for desktops only.
+				# 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"
 				;;
@@ -377,6 +487,17 @@ if [ "$setup_list_sets" -eq 1 ]; then
 	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
@@ -404,6 +525,18 @@ setup_load_answers()
 			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 ;;
 			bootloader) [ "$setup_bootloader" = none ] && [ -n "$setup_av" ] && setup_bootloader=$setup_av ;;
 			enable) setup_append_csv setup_enable "$setup_av" "$setup_enable" ;;
 			disable) setup_append_csv setup_disable "$setup_av" "$setup_disable" ;;
@@ -431,13 +564,13 @@ 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" --output-fd 1 "$@"
+	dialog --backtitle "SPS Linux installer (Splux)" --output-fd 1 "$@"
 }
 
 setup_msg()
 {
 	if [ "$setup_interactive" -eq 1 ]; then
-		dialog --backtitle "SPS Linux installer" --msgbox "$1" 12 64 || :
+		dialog --backtitle "SPS Linux installer (Splux)" --msgbox "$1" 12 64 || :
 	else
 		printf '%s\n' "$1"
 	fi
@@ -446,12 +579,184 @@ setup_msg()
 setup_confirm()
 {
 	if [ "$setup_interactive" -eq 1 ]; then
-		dialog --backtitle "SPS Linux installer" --yesno "$1" 14 64
+		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 [ -n "$setup_partition" ]; then
+		return 0
+	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)
+			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
+			;;
+	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=$(setup_dialog --title "Init system" --default-item systemd \
+		--menu "PID 1 on the installed system. Live images prefer systemd when the ISO was built with it; busybox init is the live fallback so the disc cannot panic if systemd is missing. These packages conflict; sget will refuse both." \
+		16 72 4 \
+		systemd "systemd (udev, logind, the default)" \
+		openrc "OpenRC 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" \
@@ -470,21 +775,23 @@ trap setup_cleanup 0 HUP INT TERM
 if [ "$setup_interactive" -eq 1 ]; then
 	if [ -f /etc/sps/live ]; then
 		setup_msg \
-			"This is the SPS live TTY. Mount the destination filesystem on /mnt first.
+			"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.
 
-Then continue. The installer never partitions disks. Bootloader install is a separate, explicit step."
+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 an SPS root the way Slackware and FreeBSD setup do: a sequence of dialog screens, then a visible package install.
+			"This installer configures SPS Linux (Splux) the way Slackware and FreeBSD setup do: disks, profile, kernel, drivers, then a visible package install.
 
-It never partitions disks. Point it at a mounted filesystem. Installing the bootloader is a separate, explicit step."
+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. / is the live system." \
+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
@@ -502,9 +809,12 @@ Use /mnt after mounting the destination filesystem. / is the live system." \
 			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:-sps}") ||
+			--inputbox "System hostname." 8 50 "${setup_hostname:-splux}") ||
 			setup_fail "$SPS_EX_USAGE" "cancelled"
 	fi
 
@@ -556,6 +866,8 @@ Use /mnt after mounting the destination filesystem. / is the live system." \
 					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
@@ -605,11 +917,13 @@ Use /mnt after mounting the destination filesystem. / is the live system." \
 		fi
 	fi
 
+	setup_ask_drivers
+
 	if [ -z "$setup_nvidia" ]; then
-		if setup_dialog --title "NVIDIA" --yesno \
+		if setup_dialog --title "NVIDIA proprietary" --yesno \
 			"Include NVIDIA open kernel modules and user-space utilities?
 
-This requires a matching kernel on the target and is off by default." 10 64
+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
@@ -660,13 +974,8 @@ This requires a matching kernel on the target and is off by default." 10 64
 
 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_dialog --title "Disks" --msgbox \
-		"This installer does not partition, format, or write /etc/fstab.
-
-Mount the destination filesystem on the target path before running setup. Swap and fstab remain administrator work. Notes are written to /root/sps-fstab.txt." 12 64 || :
-
 	setup_bl=$(setup_dialog --title "Bootloader" \
-		--menu "SPS never runs grub-install unless you pass --install-bootloader. The usual choice is to write notes only." \
+		--menu "SPS never runs grub-install unless you pass --install-bootloader with --disk. The usual choice is to write notes only." \
 		14 64 3 \
 		none "Write /root/sps-bootloader.txt only" \
 		grub "Write GRUB notes (still does not install)") ||
@@ -677,7 +986,7 @@ fi
 # Non-interactive defaults
 [ -n "$setup_target" ] || setup_target=/mnt
 [ -n "$setup_profile" ] || setup_profile=minimal
-[ -n "$setup_hostname" ] || setup_hostname=sps
+[ -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
@@ -686,6 +995,58 @@ 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")
+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) ;;
+	*) setup_fail "$SPS_EX_USAGE" "init must be systemd, openrc, or none" ;;
+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
+		;;
+	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
 case $setup_bootloader in
 	none|grub) ;;
 	*) setup_fail "$SPS_EX_USAGE" "bootloader must be none or grub" ;;
@@ -790,6 +1151,27 @@ for setup_item in $setup_extra; do
 	setup_add_pkg "$setup_item"
 done
 
+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 ;;
+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
@@ -808,6 +1190,18 @@ setup_write_answers_file()
 		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 '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"
@@ -835,6 +1229,11 @@ setup_summary()
 	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 '\nPackages:\n'
 	sort -u "$setup_pkg_file"
 }
@@ -849,14 +1248,43 @@ if [ "$setup_plan" -eq 1 ]; then
 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
@@ -869,6 +1297,7 @@ setup_apply_config()
 	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 || :
@@ -930,6 +1359,21 @@ Or for BIOS:
 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}
+
+systemd and OpenRC conflict; sget will not install both. 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.
+  none     no init package was requested. Busybox can still provide
+           an init applet if you copy it to /sbin/init yourself.
+EOF
+
 	cat >"$setup_root/root/sps-network.txt" <<EOF
 SPS setup does not write DHCP or static network configuration.
 
@@ -941,12 +1385,21 @@ OpenSSH is installed when the server profile or the ssh set is selected.
 EOF
 
 	cat >"$setup_root/root/sps-fstab.txt" <<EOF
-SPS setup does not partition disks or write /etc/fstab.
-
-Mount the target filesystem on the path you passed to --target (or the
-target screen) before running the installer. Add swap and extra mounts
-yourself after inspecting lsblk and writing /etc/fstab.
+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
 }
 
 # Packages first, then hostname/user so filesystem's passwd is already present.
@@ -956,6 +1409,12 @@ mkdir -p "$setup_root" ||
 
 export SPS_ROOT=$setup_root
 
+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
+
 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"
@@ -998,8 +1457,20 @@ 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"
-	setup_fail "$SPS_EX_USAGE" \
-		"--install-bootloader requires a disk argument; refusing to guess. See /root/sps-bootloader.txt"
+	[ -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
+		grub-install --target=x86_64-efi --efi-directory="$setup_root/boot/efi" \
+			--bootloader-id=SPS --boot-directory="$setup_root/boot" \
+			"$setup_disk" ||
+			setup_fail "$SPS_EX_BUILD" "grub-install (EFI) failed"
+	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.
diff --git a/bin/sget b/bin/sget
index f81e11b..bd80fd4 100755
--- a/bin/sget
+++ b/bin/sget
@@ -191,6 +191,21 @@ sget_compare_selected()
     printf '%s\n' "$sget_cmp"
 }
 
+sget_installed_names()
+{
+    sget_list=
+    [ -d "$SPS_DB/installed" ] || {
+        printf ''
+        return 0
+    }
+    for sget_d in "$SPS_DB/installed"/*; do
+        [ -d "$sget_d" ] || continue
+        sget_n=${sget_d##*/}
+        [ -n "$sget_list" ] && sget_list=$sget_list,$sget_n || sget_list=$sget_n
+    done
+    printf '%s' "$sget_list"
+}
+
 sget_cleanup_install()
 {
     if [ -n "${sget_work:-}" ]; then
@@ -300,6 +315,7 @@ sget_install()
     else
         if ! awk -v action=plan -v roots="$sget_roots" \
             -v include_build=1 -v program=sget \
+            -v installed="$(sget_installed_names)" \
             -f "$SPS_LIBDIR/version.awk" -f "$SPS_LIBDIR/deps.awk" \
             "$sps_index" >"$sget_work/resolution"; then
             sps_die "$SPS_EX_DEPENDENCY" "cannot resolve requested packages"
diff --git a/lib/deps.awk b/lib/deps.awk
index c1eefa4..88bd024 100644
--- a/lib/deps.awk
+++ b/lib/deps.awk
@@ -167,9 +167,9 @@ BEGIN {
 }
 
 {
-    if (NF != 12) {
+    if (NF != 12 && NF != 13) {
         deps_error(FILENAME ":" FNR \
-                   ": malformed index record (expected 12 fields)")
+                   ": malformed index record (expected 12 or 13 fields)")
         next
     }
     if ($1 !~ /^[A-Za-z0-9][A-Za-z0-9+_.-]*$/ ||
@@ -191,6 +191,7 @@ BEGIN {
         deps_build[$1] = $6
         deps_recipe[$1] = $10
         deps_recipe_sha256[$1] = $12
+        deps_conflict[$1] = (NF >= 13 ? $13 : "")
     } else if (($9 + 0) > deps_selected_priority[$1]) {
         deps_selected_priority[$1] = $9 + 0
         deps_version[$1] = $2
@@ -200,6 +201,7 @@ BEGIN {
         deps_build[$1] = $6
         deps_recipe[$1] = $10
         deps_recipe_sha256[$1] = $12
+        deps_conflict[$1] = (NF >= 13 ? $13 : "")
     }
 }
 
@@ -236,6 +238,42 @@ END {
     if (deps_failed)
         exit 5
 
+    # installed=name,name from sget: already-installed packages.
+    if (installed != "") {
+        deps_ninst = split(installed, deps_inst, ",")
+        for (deps_i = 1; deps_i <= deps_ninst; deps_i++) {
+            if (deps_inst[deps_i] != "")
+                deps_installed[deps_inst[deps_i]] = 1
+        }
+    }
+    for (deps_i = 1; deps_i <= deps_order_count; deps_i++)
+        deps_in_order[deps_order[deps_i]] = 1
+    for (deps_i = 1; deps_i <= deps_order_count; deps_i++) {
+        deps_package = deps_order[deps_i]
+        if (deps_conflict[deps_package] == "")
+            continue
+        deps_nc = split(deps_conflict[deps_package], deps_citems, ",")
+        for (deps_j = 1; deps_j <= deps_nc; deps_j++) {
+            deps_other = deps_citems[deps_j]
+            if (deps_other == "" || deps_other == deps_package)
+                continue
+            if (deps_in_order[deps_other]) {
+                deps_error("package '" deps_package "' conflicts with '" \
+                           deps_other "' (both in this transaction)")
+                break
+            }
+            if (deps_installed[deps_other]) {
+                deps_error("package '" deps_package "' conflicts with installed '" \
+                           deps_other "'")
+                break
+            }
+        }
+        if (deps_failed)
+            break
+    }
+    if (deps_failed)
+        exit 5
+
     if (action == "order" || action == "plan") {
         for (deps_i = 1; deps_i <= deps_order_count; deps_i++) {
             deps_package = deps_order[deps_i]
diff --git a/lib/index.awk b/lib/index.awk
index 34e16cb..5e286de 100644
--- a/lib/index.awk
+++ b/lib/index.awk
@@ -48,6 +48,8 @@ function index_print_show(name) {
                                   ? "-" : index_output_fields[6])
     print "optional dependencies: " (index_output_fields[7] == "" \
                                      ? "-" : index_output_fields[7])
+    print "conflicts: " (index_output_fields[13] == "" \
+                         ? "-" : index_output_fields[13])
     print "definition sha256: " index_output_fields[12]
 }
 
@@ -60,8 +62,8 @@ BEGIN {
 }
 
 {
-    if (NF != 12) {
-        index_error(FILENAME ":" FNR ": malformed index record (expected 12 fields)")
+    if (NF != 12 && NF != 13) {
+        index_error(FILENAME ":" FNR ": malformed index record (expected 12 or 13 fields)")
         next
     }
     if ($1 !~ /^[A-Za-z0-9][A-Za-z0-9+_.-]*$/ ||
@@ -145,7 +147,7 @@ END {
                   index_output_fields[12], index_output_fields[2],
                   index_output_fields[3], index_output_fields[4]
         } else {
-            if (field !~ /^[[:digit:]]+$/ || field < 1 || field > 12) {
+            if (field !~ /^[[:digit:]]+$/ || field < 1 || field > 13) {
                 index_stderr((program == "" ? "src" : program) \
                              ": invalid index field '" field "'")
                 exit 2
diff --git a/lib/mkiso/initrd-init b/lib/mkiso/initrd-init
index f245fd5..7eccc1d 100755
--- a/lib/mkiso/initrd-init
+++ b/lib/mkiso/initrd-init
@@ -1,75 +1,101 @@
-#!/bin/sh
-# Live initramfs. Mount the ISO, squashfs, and overlay, then switch_root.
+#!/bin/busybox sh
+# Initramfs PID 1. Find the ISO, mount squashfs+overlay, switch_root.
+#
+# Do not call `busybox --install -s` here. That writes absolute applet
+# links, and those panic once the host paths are gone. mkiso already
+# planted relative links next to /bin/busybox.
+#
+# Invoke applets as `/bin/busybox NAME` so a missing symlink cannot
+# make PID 1 exit 127.
 
 PATH=/bin:/sbin:/usr/bin:/usr/sbin
 export PATH
 
-/bin/busybox --install -s /bin 2>/dev/null || :
-# Applets may already be relative links from mkiso.
+bb=/bin/busybox
+[ -x "$bb" ] || bb=busybox
 
 mkdir -p /proc /sys /dev /tmp /mnt/iso /mnt/ro /mnt/rw /mnt/new
 
-mount -t proc proc /proc
-mount -t sysfs sysfs /sys
-mount -t devtmpfs devtmpfs /dev 2>/dev/null || mount -t tmpfs tmpfs /dev
+"$bb" mount -t proc proc /proc
+"$bb" mount -t sysfs sysfs /sys
+"$bb" mount -t devtmpfs devtmpfs /dev 2>/dev/null ||
+	"$bb" mount -t tmpfs tmpfs /dev
 mkdir -p /dev/pts
-mount -t devpts devpts /dev/pts 2>/dev/null || :
+"$bb" mount -t devpts devpts /dev/pts 2>/dev/null || :
+
+rescue()
+{
+	printf '%s\n' "$*"
+	printf '%s\n' "mkiso: dropping to a shell. PID 1 stays this shell."
+	# Do not exec a missing binary; keep this process alive.
+	while :
+	do
+		"$bb" setsid "$bb" sh -c 'exec '"$bb"' sh <>/dev/console >&0 2>&1' ||
+			"$bb" sh
+		"$bb" sleep 1
+	done
+}
 
-# Load filesystem modules when they are not built in.
 for mod in loop squashfs overlay isofs vfat nls_cp437 nls_iso8859-1 \
 	usb-storage xhci-pci ehci-pci uhci-hcd sr_mod sd_mod ahci nvme \
-	virtio_blk virtio_pci virtio_scsi virtio_net
+	virtio_blk virtio_pci virtio_scsi virtio_net virtio_console \
+	sdhci sdhci-pci mmc_block
 do
-	modprobe "$mod" 2>/dev/null || :
+	"$bb" modprobe "$mod" 2>/dev/null || :
 done
 
 sleep 1
 
 found=
-for i in 1 2 3 4 5 6 7 8 9 10; do
-	for dev in /dev/sr0 /dev/sr1 /dev/cdrom \
-		/dev/nvme0n1p1 /dev/nvme1n1p1 \
-		/dev/sda /dev/sda1 /dev/sdb /dev/sdb1 \
-		/dev/vda /dev/vda1 /dev/vdb /dev/vdb1
+i=0
+while [ "$i" -lt 15 ]
+do
+	i=$((i + 1))
+	for dev in /dev/sr0 /dev/sr1 /dev/sr2 /dev/cdrom \
+		/dev/nvme0n1 /dev/nvme0n1p1 /dev/nvme1n1p1 \
+		/dev/sda /dev/sda1 /dev/sdb /dev/sdb1 /dev/sdc /dev/sdc1 \
+		/dev/vda /dev/vda1 /dev/vdb /dev/vdb1 \
+		/dev/xvda /dev/xvda1
 	do
 		[ -b "$dev" ] || continue
-		if mount -t iso9660 -o ro "$dev" /mnt/iso 2>/dev/null ||
-		   mount -o ro "$dev" /mnt/iso 2>/dev/null; then
+		if "$bb" mount -t iso9660 -o ro "$dev" /mnt/iso 2>/dev/null ||
+		   "$bb" mount -o ro "$dev" /mnt/iso 2>/dev/null; then
 			if [ -f /mnt/iso/live/filesystem.squashfs ]; then
 				found=$dev
 				break
 			fi
-			umount /mnt/iso 2>/dev/null || :
+			"$bb" umount /mnt/iso 2>/dev/null || :
 		fi
 	done
 	[ -n "$found" ] && break
 	sleep 1
 done
 
-if [ -z "$found" ]; then
-	printf '%s\n' "mkiso: cannot find the SPS live filesystem on a CD/USB."
-	printf '%s\n' "Dropping to a shell."
-	exec setsid /bin/sh </dev/console >/dev/console 2>&1
-fi
+[ -n "$found" ] || rescue "mkiso: cannot find the SPS Linux live filesystem on a CD/USB."
 
-mount -t squashfs -o ro /mnt/iso/live/filesystem.squashfs /mnt/ro || {
-	printf '%s\n' "mkiso: squashfs mount failed"
-	exec setsid /bin/sh </dev/console >/dev/console 2>&1
-}
+"$bb" mount -t squashfs -o ro /mnt/iso/live/filesystem.squashfs /mnt/ro ||
+	rescue "mkiso: squashfs mount failed"
 
-mount -t tmpfs tmpfs /mnt/rw
+"$bb" mount -t tmpfs tmpfs /mnt/rw
 mkdir -p /mnt/rw/upper /mnt/rw/work
-mount -t overlay overlay \
+"$bb" mount -t overlay overlay \
 	-o lowerdir=/mnt/ro,upperdir=/mnt/rw/upper,workdir=/mnt/rw/work \
-	/mnt/new || {
-	printf '%s\n' "mkiso: overlay mount failed"
-	exec setsid /bin/sh </dev/console >/dev/console 2>&1
-}
+	/mnt/new ||
+	rescue "mkiso: overlay mount failed"
 
-mkdir -p /mnt/new/mnt/iso /mnt/new/proc /mnt/new/sys /mnt/new/dev /mnt/new/run /mnt/new/tmp
-mount --move /mnt/iso /mnt/new/mnt/iso 2>/dev/null || :
-mount --move /proc /mnt/new/proc 2>/dev/null || :
-mount --move /sys /mnt/new/sys 2>/dev/null || :
-mount --move /dev /mnt/new/dev 2>/dev/null || :
+mkdir -p /mnt/new/mnt/iso /mnt/new/proc /mnt/new/sys /mnt/new/dev \
+	/mnt/new/run /mnt/new/tmp
+"$bb" mount --move /mnt/iso /mnt/new/mnt/iso 2>/dev/null || :
+"$bb" mount --move /proc /mnt/new/proc 2>/dev/null || :
+"$bb" mount --move /sys /mnt/new/sys 2>/dev/null || :
+"$bb" mount --move /dev /mnt/new/dev 2>/dev/null || :
+
+if [ ! -e /mnt/new/sbin/init ] && [ ! -L /mnt/new/sbin/init ]; then
+	rescue "mkiso: /sbin/init missing on the live filesystem"
+fi
 
-exec switch_root /mnt/new /sbin/init
+# busybox switch_root replaces PID 1. If the new init cannot exec, we
+# would panic; the live /sbin/init is an ELF (busybox or systemd), not
+# a script that execs a console.
+exec "$bb" switch_root /mnt/new /sbin/init
+rescue "mkiso: switch_root failed"
diff --git a/lib/mkiso/inittab b/lib/mkiso/inittab
new file mode 100644
index 0000000..7755f12
--- /dev/null
+++ b/lib/mkiso/inittab
@@ -0,0 +1,12 @@
+# SPS Linux live busybox inittab.
+# PID 1 is busybox init (an ELF). Consoles are children, not exec targets.
+# The previous live TTY exec'd a shell as PID 1 and died in QEMU when
+# that shell exited 127 (missing setsid/getty/sh, or ttyS0 hangup).
+
+::sysinit:/etc/sps/live-rc
+tty1::respawn:/bin/busybox getty -n -l /bin/sh 38400 tty1
+tty2::respawn:/bin/busybox getty -n -l /bin/sh 38400 tty2
+ttyS0::respawn:/bin/busybox getty -n -l /bin/sh -L 115200 ttyS0
+::ctrlaltdel:/bin/busybox reboot
+::shutdown:/bin/busybox umount -a -r
+::restart:/sbin/init
diff --git a/lib/mkiso/inittab-plasma b/lib/mkiso/inittab-plasma
new file mode 100644
index 0000000..5429000
--- /dev/null
+++ b/lib/mkiso/inittab-plasma
@@ -0,0 +1,8 @@
+# SPS Linux Plasma live: Wayland session on tty1 instead of a getty.
+::sysinit:/etc/sps/live-rc
+tty1::respawn:/etc/sps/live-plasma
+tty2::respawn:/bin/busybox getty -n -l /bin/sh 38400 tty2
+ttyS0::respawn:/bin/busybox getty -n -l /bin/sh -L 115200 ttyS0
+::ctrlaltdel:/bin/busybox reboot
+::shutdown:/bin/busybox umount -a -r
+::restart:/sbin/init
diff --git a/lib/mkiso/issue b/lib/mkiso/issue
index fd669f6..42e1aac 100644
--- a/lib/mkiso/issue
+++ b/lib/mkiso/issue
@@ -1,3 +1,3 @@
-SPS Linux live (console)
+SPS Linux live (Splux)
 
 Mount a destination filesystem on /mnt, then run: setup
diff --git a/lib/mkiso/live-init b/lib/mkiso/live-init
index 491b29b..aedbd16 100755
--- a/lib/mkiso/live-init
+++ b/lib/mkiso/live-init
@@ -1,72 +1,29 @@
 #!/bin/sh
-# PID 1 on the live squashfs. Bring up consoles and optionally start setup.
+# Compatibility wrapper. mkiso normally installs busybox or systemd as
+# /sbin/init (an ELF). If this script is still PID 1, hand off to a real
+# init. Do not exec a console here; that killed PID 1 on the old live TTY.
 
-PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
-if [ -d /usr/lib64/go1.27.0/go/bin ]; then
-	PATH=/usr/lib64/go1.27.0/go/bin:$PATH
-fi
+PATH=/bin:/sbin:/usr/bin:/usr/sbin
 export PATH
-export HOME=/root
-export TERM=${TERM:-linux}
-export CARGO_HOME=${CARGO_HOME:-/root/.cargo}
-
-mkdir -p /proc /sys /dev /run /tmp /root /mnt /etc/sps
-mount -t proc proc /proc 2>/dev/null || :
-mount -t sysfs sysfs /sys 2>/dev/null || :
-mount -t devtmpfs devtmpfs /dev 2>/dev/null || :
-mkdir -p /dev/pts /dev/shm
-mount -t devpts devpts /dev/pts 2>/dev/null || :
-mount -t tmpfs tmpfs /run 2>/dev/null || :
-mount -t tmpfs tmpfs /tmp 2>/dev/null || :
 
-if command -v ldconfig >/dev/null 2>&1; then
-	ldconfig 2>/dev/null || :
+if [ -x /usr/lib/systemd/systemd ]; then
+	exec /usr/lib/systemd/systemd "$@"
 fi
-
-ln -sf /proc/self/fd /dev/fd 2>/dev/null || :
-ln -sf /proc/self/fd/0 /dev/stdin 2>/dev/null || :
-ln -sf /proc/self/fd/1 /dev/stdout 2>/dev/null || :
-ln -sf /proc/self/fd/2 /dev/stderr 2>/dev/null || :
-
-hostname sps-live 2>/dev/null || :
-[ -f /etc/hostname ] || printf '%s\n' sps-live >/etc/hostname
-
-# Load remaining modules from the live image if present.
-if [ -d /lib/modules ] && command -v depmod >/dev/null 2>&1; then
-	depmod -a 2>/dev/null || :
+if [ -x /lib/systemd/systemd ]; then
+	exec /lib/systemd/systemd "$@"
 fi
-for mod in e1000 e1000e r8169 virtio_net usbnet hid-generic usbhid atkbd
-do
-	modprobe "$mod" 2>/dev/null || :
-done
-
-if command -v ip >/dev/null 2>&1; then
-	ip link set lo up 2>/dev/null || :
+if [ -x /sbin/openrc-init ]; then
+	exec /sbin/openrc-init "$@"
 fi
-if command -v dhcpcd >/dev/null 2>&1; then
-	dhcpcd -b 2>/dev/null || :
-elif command -v dhclient >/dev/null 2>&1; then
-	dhclient -nw 2>/dev/null || :
+if [ -x /bin/busybox ]; then
+	exec /bin/busybox init
 fi
 
-cat /etc/issue 2>/dev/null || :
-cat /etc/motd 2>/dev/null || :
-
-cmdline=$(cat /proc/cmdline 2>/dev/null || :)
-case " $cmdline " in
-	*" sps=setup "*)
-		if command -v setup >/dev/null 2>&1; then
-			setup || :
-		fi
-		;;
-esac
-
-# Consoles: a root shell. There is no login prompt on the live image.
-# ttyS0 is for qemu -nographic; tty1 is a hardware VGA console.
-if [ -c /dev/tty1 ]; then
-	setsid /bin/sh -c 'exec /bin/sh <>/dev/tty1 >&0 2>&1' &
-fi
-if [ -c /dev/ttyS0 ]; then
-	exec setsid /bin/sh -c 'exec /bin/sh <>/dev/ttyS0 >&0 2>&1'
-fi
-exec /bin/sh </dev/console >/dev/console 2>&1
+# Last resort: stay PID 1 and spawn shells as children.
+while :
+do
+	[ -c /dev/tty1 ] && setsid /bin/sh -c 'exec /bin/sh <>/dev/tty1 >&0 2>&1' &
+	[ -c /dev/ttyS0 ] && setsid /bin/sh -c 'exec /bin/sh <>/dev/ttyS0 >&0 2>&1' &
+	wait
+	sleep 1
+done
diff --git a/lib/mkiso/live-plasma b/lib/mkiso/live-plasma
new file mode 100644
index 0000000..6595087
--- /dev/null
+++ b/lib/mkiso/live-plasma
@@ -0,0 +1,38 @@
+#!/bin/sh
+# Plasma live session. Respawned by busybox init on tty1. Open Konsole
+# and run setup(1). If Plasma is missing, a shell stays on this tty.
+
+PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+export PATH
+export HOME=/root
+export XDG_RUNTIME_DIR=/run/user/0
+export XDG_SESSION_TYPE=wayland
+export QT_QPA_PLATFORM=wayland
+export TERM=${TERM:-linux}
+
+mkdir -p "$XDG_RUNTIME_DIR"
+chmod 700 "$XDG_RUNTIME_DIR"
+
+# Live NVIDIA is nouveau. Keep the proprietary stack unloaded.
+modprobe nouveau 2>/dev/null || :
+
+if command -v dbus-daemon >/dev/null 2>&1; then
+	if [ ! -S /run/dbus/system_bus_socket ]; then
+		mkdir -p /run/dbus
+		dbus-daemon --system --fork 2>/dev/null || :
+	fi
+fi
+if command -v seatd >/dev/null 2>&1; then
+	seatd -g video >/dev/null 2>&1 &
+	sleep 1
+fi
+
+if command -v startplasma-wayland >/dev/null 2>&1; then
+	# Give the user a console they can see if Wayland dies.
+	chvt 1 2>/dev/null || :
+	exec startplasma-wayland
+fi
+
+printf '%s\n' "Plasma is not on this image; dropping to a shell on tty1."
+printf '%s\n' "Run setup from here, or use tty2 / ttyS0."
+exec /bin/sh
diff --git a/lib/mkiso/live-rc b/lib/mkiso/live-rc
new file mode 100644
index 0000000..4515cac
--- /dev/null
+++ b/lib/mkiso/live-rc
@@ -0,0 +1,90 @@
+#!/bin/sh
+# Sysinit for the live image. Child of busybox init, or a systemd oneshot.
+# Must not replace PID 1 and must not exec a console. The old live TTY did
+# that and the kernel reported "Attempted to kill init" (exit 127).
+
+PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+if [ -d /usr/lib64/go1.27.0/go/bin ]; then
+	PATH=/usr/lib64/go1.27.0/go/bin:$PATH
+fi
+export PATH
+export HOME=/root
+export TERM=${TERM:-linux}
+export CARGO_HOME=${CARGO_HOME:-/root/.cargo}
+
+mkdir -p /proc /sys /dev /run /tmp /root /mnt /etc/sps
+mount -t proc proc /proc 2>/dev/null || :
+mount -t sysfs sysfs /sys 2>/dev/null || :
+mount -t devtmpfs devtmpfs /dev 2>/dev/null || :
+mkdir -p /dev/pts /dev/shm
+mount -t devpts devpts /dev/pts 2>/dev/null || :
+mount -t tmpfs tmpfs /run 2>/dev/null || :
+mount -t tmpfs tmpfs /tmp 2>/dev/null || :
+
+if command -v ldconfig >/dev/null 2>&1; then
+	ldconfig 2>/dev/null || :
+fi
+
+ln -sf /proc/self/fd /dev/fd 2>/dev/null || :
+ln -sf /proc/self/fd/0 /dev/stdin 2>/dev/null || :
+ln -sf /proc/self/fd/1 /dev/stdout 2>/dev/null || :
+ln -sf /proc/self/fd/2 /dev/stderr 2>/dev/null || :
+
+hostname splux-live 2>/dev/null || :
+[ -f /etc/hostname ] || printf '%s\n' splux-live >/etc/hostname
+
+# NVIDIA live path is nouveau. Do not load proprietary nvidia* next to KWin.
+mkdir -p /etc/modprobe.d
+if [ ! -f /etc/modprobe.d/sps-nouveau.conf ]; then
+	printf '%s\n' \
+		'blacklist nvidia' \
+		'blacklist nvidia_drm' \
+		'blacklist nvidia_modeset' \
+		'blacklist nvidia_uvm' \
+		'options nouveau modeset=1' \
+		>/etc/modprobe.d/sps-nouveau.conf
+fi
+
+if [ -d /lib/modules ] && command -v depmod >/dev/null 2>&1; then
+	depmod -a 2>/dev/null || :
+fi
+for mod in e1000 e1000e r8169 virtio_net usbnet hid-generic usbhid atkbd \
+	nouveau bochs drm_kms_helper
+do
+	modprobe "$mod" 2>/dev/null || :
+done
+
+if command -v ip >/dev/null 2>&1; then
+	ip link set lo up 2>/dev/null || :
+fi
+if command -v dhcpcd >/dev/null 2>&1; then
+	dhcpcd -b 2>/dev/null || :
+elif command -v dhclient >/dev/null 2>&1; then
+	dhclient -nw 2>/dev/null || :
+fi
+
+# udev if present (eudev or systemd-udevd).
+if command -v udevd >/dev/null 2>&1; then
+	udevd --daemon 2>/dev/null || :
+	udevadm trigger --action=add 2>/dev/null || :
+	udevadm settle --timeout=5 2>/dev/null || :
+elif [ -x /lib/systemd/systemd-udevd ]; then
+	/lib/systemd/systemd-udevd --daemon 2>/dev/null || :
+	udevadm trigger --action=add 2>/dev/null || :
+	udevadm settle --timeout=5 2>/dev/null || :
+fi
+
+cat /etc/issue 2>/dev/null || :
+cat /etc/motd 2>/dev/null || :
+
+cmdline=$(cat /proc/cmdline 2>/dev/null || :)
+case " $cmdline " in
+	*" sps=setup "*)
+		# Child, not PID 1. setup can fail without taking the box down.
+		if command -v setup >/dev/null 2>&1; then
+			setup || :
+		fi
+		;;
+esac
+
+exit 0
diff --git a/lib/mkiso/motd b/lib/mkiso/motd
index 595f2ba..64f7316 100644
--- a/lib/mkiso/motd
+++ b/lib/mkiso/motd
@@ -1,20 +1,19 @@
-SPS Linux live TTY
+SPS Linux live (Splux)
 
-This disc boots a console. It does not partition disks.
+This disc boots with a real init as PID 1: systemd if the machine that
+built the ISO had it, otherwise busybox init with gettys on tty1 and
+ttyS0. The shell on those ttys is a child of init, not init itself.
 
-1. Plug in or identify the destination disk (lsblk).
-2. Partition and format it yourself, then mount it on /mnt.
-3. Run:  setup --target /mnt
-   Or just: setup
-   and point the target screen at /mnt.
+1. Identify the destination disk (lsblk).
+2. Run:  setup
+   Guided partitioning is in the installer. You can also mount a
+   filesystem on /mnt and choose "use existing mounts".
 
-Optional: setup --plan --profile plasma-full --target /mnt
+setup asks which init to install: systemd (default) or OpenRC.
+Those packages conflict; sget will not install both.
 
-Recipes for sps-core and sps-extra are in /usr/src/sps. Package
-builds run on this live system into the mounted target. A Plasma
-install compiles Qt and KDE from source and takes a long time.
+On a Plasma live image, open Konsole and run setup. On this TTY image,
+run it from the shell.
 
-Kernel modules match the kernel on this disc. NVIDIA modules still
-need that kernel after the first boot of the installed system.
-
-Root has an open shell on the live image. Set passwords after install.
+SPS Linux is the distro (informally Splux). SPS the package system is
+separate; other projects can use it without this live image or installer.
diff --git a/lib/mkiso/plasma-bins b/lib/mkiso/plasma-bins
new file mode 100644
index 0000000..86aaf59
--- /dev/null
+++ b/lib/mkiso/plasma-bins
@@ -0,0 +1,33 @@
+# Binaries copied into a --session plasma live image from the host.
+# mkiso does not compile Qt or Plasma; it only copies a stack that already
+# works on the machine building the ISO. Konsole is the supported way to
+# run setup(1) from the live session.
+/usr/bin/startplasma-wayland
+/usr/bin/startplasma-x11
+/usr/bin/kwin_wayland
+/usr/bin/kwin_x11
+/usr/bin/plasmashell
+/usr/bin/konsole
+/usr/bin/dolphin
+/usr/bin/kwrite
+/usr/bin/kate
+/usr/bin/kded6
+/usr/bin/kdeinit6
+/usr/bin/kglobalacceld
+/usr/bin/kcminit
+/usr/bin/ksmserver
+/usr/bin/krunner
+/usr/bin/systemsettings
+/usr/bin/dbus-daemon
+/usr/bin/dbus-launch
+/usr/bin/dbus-run-session
+/usr/bin/dbus-uuidgen
+/usr/bin/seatd
+/usr/bin/Xorg
+/usr/bin/xauth
+/usr/bin/xrdb
+/usr/bin/xset
+/usr/bin/setxkbmap
+/usr/bin/pipewire
+/usr/bin/pipewire-pulse
+/usr/bin/wireplumber
diff --git a/lib/mkiso/plasma-motd b/lib/mkiso/plasma-motd
new file mode 100644
index 0000000..1210f06
--- /dev/null
+++ b/lib/mkiso/plasma-motd
@@ -0,0 +1,20 @@
+SPS Linux live Plasma (Splux)
+
+This disc boots a Plasma session on Wayland if the machine that built
+it already had a working Plasma stack. NVIDIA hardware uses nouveau;
+proprietary nvidia modules are blacklisted so they cannot load next
+to KWin.
+
+Open Konsole and run:
+
+    setup
+
+Guided partitioning is in setup. If Plasma does not start, switch to
+tty2 (Ctrl-Alt-F2) for a root shell.
+
+Optional: setup --plan --profile plasma-full --target /mnt
+
+Recipes for sps-core and sps-extra are in /usr/src/sps. Package builds
+run on this live system into the mounted target. A from-source Plasma
+install compiles Qt and KDE and takes a long time. This live session is
+the host stack copied at ISO build time.
diff --git a/lib/mkiso/plasma-trees b/lib/mkiso/plasma-trees
new file mode 100644
index 0000000..e745617
--- /dev/null
+++ b/lib/mkiso/plasma-trees
@@ -0,0 +1,39 @@
+# Directory trees copied into a --session plasma live image.
+# Keep this list to prefixes the live session actually needs. Copying all of
+# /usr is how you accidentally ship a 4G ISO.
+/usr/lib64/qt6
+/usr/lib/qt6
+/usr/share/qt6
+/usr/share/plasma
+/usr/share/kservicetypes6
+/usr/share/kservices6
+/usr/share/qlogging-categories6
+/usr/share/wayland-sessions
+/usr/share/xsessions
+/usr/share/konsole
+/usr/share/applications
+/usr/share/desktop-directories
+/usr/share/icons
+/usr/share/mime
+/usr/share/knotifications6
+/usr/share/kglobalaccel
+/usr/share/kded6
+/usr/share/dbus-1
+/usr/share/polkit-1
+/usr/share/wayland-protocols
+/usr/share/libdrm
+/usr/share/X11
+/usr/lib64/xorg
+/usr/lib/xorg
+/etc/X11
+/etc/xdg
+/usr/lib64/dri
+/usr/lib/dri
+/usr/lib64/gbm
+/usr/lib64/libkwin.so
+/lib/firmware/nvidia
+/lib/firmware/nouveau
+/usr/lib64/pipewire-0.3
+/usr/lib64/spa-0.2
+/usr/share/pipewire
+/usr/share/wireplumber
diff --git a/lib/mkiso/seed-bins b/lib/mkiso/seed-bins
index c6b3be3..8517465 100644
--- a/lib/mkiso/seed-bins
+++ b/lib/mkiso/seed-bins
@@ -125,3 +125,7 @@
 /usr/lib64/libstdc++.so.6
 /usr/lib64/libgomp.so.1
 /usr/lib64/libatomic.so.1
+/usr/bin/setsid
+/sbin/agetty
+/usr/bin/getty
+
diff --git a/lib/mkiso/systemd/sps-live-plasma.service b/lib/mkiso/systemd/sps-live-plasma.service
new file mode 100644
index 0000000..3da6937
--- /dev/null
+++ b/lib/mkiso/systemd/sps-live-plasma.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=SPS Linux live Plasma (Wayland)
+After=sps-live-rc.service
+Conflicts=getty@tty1.service
+
+[Service]
+Type=simple
+TTYPath=/dev/tty1
+TTYReset=yes
+TTYVHangup=yes
+StandardInput=tty
+StandardOutput=tty
+ExecStart=/etc/sps/live-plasma
+Restart=on-failure
+RestartSec=2
+
+[Install]
+WantedBy=graphical.target
diff --git a/lib/mkiso/systemd/sps-live-rc.service b/lib/mkiso/systemd/sps-live-rc.service
new file mode 100644
index 0000000..9892122
--- /dev/null
+++ b/lib/mkiso/systemd/sps-live-rc.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=SPS Linux live sysinit
+DefaultDependencies=no
+After=systemd-udevd.service
+Before=getty.target
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/etc/sps/live-rc
+StandardOutput=journal+console
+
+[Install]
+WantedBy=multi-user.target
+WantedBy=graphical.target
diff --git a/lib/mkiso/systemd/sps-live-serial.service b/lib/mkiso/systemd/sps-live-serial.service
new file mode 100644
index 0000000..c099776
--- /dev/null
+++ b/lib/mkiso/systemd/sps-live-serial.service
@@ -0,0 +1,19 @@
+[Unit]
+Description=SPS Linux live serial getty
+After=sps-live-rc.service
+ConditionPathExists=/dev/ttyS0
+
+[Service]
+ExecStart=-/sbin/agetty -n -l /bin/sh -L 115200 ttyS0
+Type=idle
+Restart=always
+RestartSec=1
+TTYPath=/dev/ttyS0
+TTYReset=yes
+TTYVHangup=yes
+IgnoreSIGPIPE=no
+SendSIGHUP=yes
+
+[Install]
+WantedBy=multi-user.target
+WantedBy=graphical.target
diff --git a/lib/recipe.awk b/lib/recipe.awk
index e3e1a1d..6ae3952 100644
--- a/lib/recipe.awk
+++ b/lib/recipe.awk
@@ -115,7 +115,8 @@ function recipe_process(line,    key, value, separator) {
     if (key == "name" || key == "version" || key == "release" ||
         key == "arch" || key == "description") {
         recipe_set_once(key, value)
-    } else if (key == "depend" || key == "builddep" || key == "optional") {
+    } else if (key == "depend" || key == "builddep" || key == "optional" ||
+               key == "conflict") {
         recipe_append_dependency(key, value)
     } else if (key == "source") {
         recipe_source_count++
@@ -181,6 +182,7 @@ END {
     recipe_value["depend"] = recipe_expand(recipe_value["depend"])
     recipe_value["builddep"] = recipe_expand(recipe_value["builddep"])
     recipe_value["optional"] = recipe_expand(recipe_value["optional"])
+    recipe_value["conflict"] = recipe_expand(recipe_value["conflict"])
 
     for (recipe_i = 1; recipe_i <= recipe_hash_count; recipe_i++) {
         recipe_hash_value = recipe_expand(recipe_hash[recipe_i])
@@ -213,6 +215,7 @@ END {
     recipe_valid_dependency_list("dependency", recipe_value["depend"])
     recipe_valid_dependency_list("build dependency", recipe_value["builddep"])
     recipe_valid_dependency_list("optional dependency", recipe_value["optional"])
+    recipe_valid_dependency_list("conflict", recipe_value["conflict"])
 
     if (recipe_repo !~ /^[[:alnum:]][[:alnum:].+_-]*$/)
         recipe_error("invalid repository name '" recipe_repo "'")
@@ -231,5 +234,6 @@ END {
           recipe_value["release"], recipe_value["arch"],
           recipe_value["depend"], recipe_value["builddep"],
           recipe_value["optional"], recipe_repo, recipe_priority,
-          recipe_path, recipe_value["description"], tolower(recipe_sha256)
+          recipe_path, recipe_value["description"], tolower(recipe_sha256),
+          recipe_value["conflict"]
 }
diff --git a/lib/relations.awk b/lib/relations.awk
index e2d27dc..12f51fd 100644
--- a/lib/relations.awk
+++ b/lib/relations.awk
@@ -56,7 +56,7 @@ function rel_find(cur, target,    i,d) {
 BEGIN { FS=OFS="\t" }
 /^$/ { next }
 {
-    if (NF != 12) { rel_err(FILENAME ":" FNR ": malformed index record"); next }
+    if (NF != 12 && NF != 13) { rel_err(FILENAME ":" FNR ": malformed index record"); next }
     name=$1; prio=$9+0
     if (!(name in selected) || prio > priority[name]) {
         selected[name]=1
diff --git a/lib/repository.awk b/lib/repository.awk
index 9f943ad..3df82a5 100644
--- a/lib/repository.awk
+++ b/lib/repository.awk
@@ -152,7 +152,8 @@ END {
                 repository_index_fields_count = split(repository_index_line,
                                                       repository_index_fields,
                                                       "\t")
-                if (repository_index_fields_count == 12)
+                if (repository_index_fields_count == 12 ||
+                    repository_index_fields_count == 13)
                     repository_package_count[repository_index_fields[8]]++
             }
             if (repository_getline == 0)
diff --git a/lib/setup/extras b/lib/setup/extras
index b6269f9..1b14166 100644
--- a/lib/setup/extras
+++ b/lib/setup/extras
@@ -69,4 +69,22 @@ vulkan-tools	vulkaninfo
 ethtool	ethtool
 nftables	nftables
 wireguard-tools	WireGuard tools
+intel-ucode	Intel CPU microcode
+amd-ucode	AMD CPU microcode
+nouveau	Nouveau (NVIDIA open driver)
+linux-desktop	Linux kernel, daily desktop config
+linux	Linux kernel, upstream defconfig
+xorg-server	X.Org server
+mpv	mpv media player
+gimp	GNU Image Manipulation Program
+inkscape	Inkscape
+libreoffice	LibreOffice
+krita	Krita
+thunderbird	Thunderbird
+qemu	QEMU
+htop	htop
+btop	btop
+tmux	tmux
+screen	GNU Screen
+
 
diff --git a/lib/setup/hwdetect.sh b/lib/setup/hwdetect.sh
new file mode 100644
index 0000000..379f3da
--- /dev/null
+++ b/lib/setup/hwdetect.sh
@@ -0,0 +1,103 @@
+# Hardware -> package mapping for the SPS Linux installer.
+#
+# Sourced, not executed. Reads lspci -nn and /proc/cpuinfo. The live ISO and
+# an installed system both have pciutils in the usual place; if lspci is
+# missing we just print nothing and the installer leaves drivers off.
+
+setup_hw_lspci()
+{
+	PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH
+	export PATH
+	if command -v lspci >/dev/null 2>&1; then
+		lspci -nn 2>/dev/null || :
+	fi
+}
+
+setup_hw_cpu_vendor()
+{
+	awk -F: '
+		tolower($1) ~ /vendor_id/ {
+			gsub(/^[ \t]+|[ \t]+$/, "", $2)
+			print $2
+			exit
+		}
+	' /proc/cpuinfo 2>/dev/null || :
+}
+
+# Print package names, one per line. Idempotent; caller dedupes.
+setup_hw_packages()
+{
+	setup_pci=$(setup_hw_lspci)
+	setup_cpu=$(setup_hw_cpu_vendor)
+
+	# CPU microcode. Intel and AMD both want the matching early-load blob.
+	case $setup_cpu in
+		GenuineIntel) printf '%s\n' intel-ucode ;;
+		AuthenticAMD) printf '%s\n' amd-ucode ;;
+	esac
+
+	# Always useful on real hardware; cheap no-op on a VM without blobs.
+	printf '%s\n' linux-firmware mesa
+
+	# GPUs. Nouveau is the live/install default for NVIDIA. The proprietary
+	# stack is a separate yes/no in setup; we do not pull it in from here.
+	if printf '%s\n' "$setup_pci" | grep -q 'VGA compatible controller.*\[10de:'; then
+		printf '%s\n' nouveau
+	fi
+	if printf '%s\n' "$setup_pci" | grep -q '3D controller.*\[10de:'; then
+		printf '%s\n' nouveau
+	fi
+	if printf '%s\n' "$setup_pci" | grep -q 'VGA compatible controller.*\[8086:'; then
+		printf '%s\n' mesa
+	fi
+	if printf '%s\n' "$setup_pci" | grep -q 'VGA compatible controller.*\[1002:'; then
+		printf '%s\n' mesa amd-ucode
+	fi
+
+	# Intel wireless (CNVi and the usual Wi-Fi class).
+	if printf '%s\n' "$setup_pci" | grep -q 'Network controller.*\[8086:'; then
+		printf '%s\n' iwd linux-firmware
+	fi
+	if printf '%s\n' "$setup_pci" | grep -q '\[0280\].*\[8086:'; then
+		printf '%s\n' iwd linux-firmware
+	fi
+
+	# Broadcom / Realtek wireless: firmware plus iwd is enough to associate.
+	if printf '%s\n' "$setup_pci" | grep -q 'Network controller.*\[14e4:'; then
+		printf '%s\n' iwd linux-firmware
+	fi
+	if printf '%s\n' "$setup_pci" | grep -q 'Network controller.*\[10ec:'; then
+		printf '%s\n' iwd linux-firmware
+	fi
+
+	# Wired Intel often just needs e1000e in the kernel; still pull firmware.
+	if printf '%s\n' "$setup_pci" | grep -q 'Ethernet controller.*\[8086:'; then
+		printf '%s\n' linux-firmware
+	fi
+
+	# Audio: PipeWire userspace is in the Plasma profiles; ALSA utils help
+	# on a console box.
+	if printf '%s\n' "$setup_pci" | grep -q 'Audio device'; then
+		printf '%s\n' alsa-utils
+	fi
+
+	# NVMe / SATA management tools when those controllers are present.
+	if printf '%s\n' "$setup_pci" | grep -q 'Non-Volatile memory controller'; then
+		printf '%s\n' nvme-cli
+	fi
+	if printf '%s\n' "$setup_pci" | grep -q 'SATA controller'; then
+		printf '%s\n' smartmontools
+	fi
+}
+
+setup_hw_summary()
+{
+	setup_pci=$(setup_hw_lspci)
+	setup_cpu=$(setup_hw_cpu_vendor)
+	printf 'CPU vendor: %s\n' "${setup_cpu:-unknown}"
+	printf '%s\n' "$setup_pci" | awk '
+		/VGA compatible controller|3D controller|Network controller|Ethernet controller|Audio device|SATA controller|Non-Volatile memory/ {
+			print
+		}
+	'
+}
diff --git a/lib/setup/partition.sh b/lib/setup/partition.sh
new file mode 100644
index 0000000..23e2f5f
--- /dev/null
+++ b/lib/setup/partition.sh
@@ -0,0 +1,282 @@
+# Disk helpers for the SPS Linux (Splux) installer.
+#
+# This is sourced, not executed. Guided layouts run sfdisk(8) and mkfs only
+# after the administrator has confirmed twice. setup never calls grub-install
+# from here; bootloader install is a separate, explicit step.
+#
+# PATH on a live image often omits /sbin. Look there first.
+
+setup_part_path()
+{
+	setup_old_path=$PATH
+	PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH
+	export PATH
+	command -v "$1" 2>/dev/null || {
+		PATH=$setup_old_path
+		return 1
+	}
+}
+
+setup_partname()
+{
+	# nvme0n1 -> nvme0n1p1; sda -> sda1
+	setup_disk=$1
+	setup_n=$2
+	case $setup_disk in
+		*[0-9]) printf '%sp%s\n' "$setup_disk" "$setup_n" ;;
+		*) printf '%s%s\n' "$setup_disk" "$setup_n" ;;
+	esac
+}
+
+setup_lsblk()
+{
+	setup_part_path lsblk >/dev/null || return 1
+	lsblk "$@"
+}
+
+setup_disk_menu_lines()
+{
+	# NAME SIZE MODEL TRAN, one disk per line. Skip loop/rom.
+	setup_lsblk -dn -p -o NAME,SIZE,TYPE,TRAN,MODEL 2>/dev/null |
+		awk '$3 == "disk" { print }'
+}
+
+setup_partition_tool_ok()
+{
+	setup_part_path sfdisk >/dev/null || return 1
+	setup_part_path mkfs.ext4 >/dev/null || return 1
+	setup_part_path mkfs.vfat >/dev/null || return 1
+	setup_part_path mkswap >/dev/null || return 1
+	return 0
+}
+
+setup_wipe_disk()
+{
+	setup_d=$1
+	if setup_part_path wipefs >/dev/null; then
+		wipefs -a "$setup_d" >/dev/null 2>&1 || :
+	fi
+}
+
+setup_sfdisk_apply()
+{
+	setup_d=$1
+	setup_script=$2
+	setup_part_path sfdisk >/dev/null || return 1
+	sfdisk --wipe always --wipe-partitions always "$setup_d" <"$setup_script"
+}
+
+setup_write_guided_script()
+{
+	# $1 = dest file, $2 = layout, $3 = swap size (sfdisk size token, e.g. 8GiB)
+	setup_out=$1
+	setup_layout=$2
+	setup_swap=${3:-8GiB}
+	case $setup_layout in
+		efi-root)
+			cat >"$setup_out" <<'EOF'
+label: gpt
+size=512MiB, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, name=EFI
+type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name=root
+EOF
+			;;
+		efi-swap-root)
+			cat >"$setup_out" <<EOF
+label: gpt
+size=512MiB, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, name=EFI
+size=$setup_swap, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F, name=swap
+type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name=root
+EOF
+			;;
+		efi-swap-root-home)
+			cat >"$setup_out" <<EOF
+label: gpt
+size=512MiB, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, name=EFI
+size=$setup_swap, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F, name=swap
+size=48GiB, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name=root
+type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name=home
+EOF
+			;;
+		bios-root)
+			cat >"$setup_out" <<'EOF'
+label: gpt
+size=1MiB, type=21686148-6449-6E6F-744E-656564454649, name=BIOS
+type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name=root
+EOF
+			;;
+		*)
+			return 1
+			;;
+	esac
+}
+
+setup_assign_guided_parts()
+{
+	setup_d=$1
+	setup_layout=$2
+	setup_part_efi=
+	setup_part_swap=
+	setup_part_root=
+	setup_part_home=
+	setup_part_bios=
+	case $setup_layout in
+		efi-root)
+			setup_part_efi=$(setup_partname "$setup_d" 1)
+			setup_part_root=$(setup_partname "$setup_d" 2)
+			;;
+		efi-swap-root)
+			setup_part_efi=$(setup_partname "$setup_d" 1)
+			setup_part_swap=$(setup_partname "$setup_d" 2)
+			setup_part_root=$(setup_partname "$setup_d" 3)
+			;;
+		efi-swap-root-home)
+			setup_part_efi=$(setup_partname "$setup_d" 1)
+			setup_part_swap=$(setup_partname "$setup_d" 2)
+			setup_part_root=$(setup_partname "$setup_d" 3)
+			setup_part_home=$(setup_partname "$setup_d" 4)
+			;;
+		bios-root)
+			setup_part_bios=$(setup_partname "$setup_d" 1)
+			setup_part_root=$(setup_partname "$setup_d" 2)
+			;;
+		*)
+			return 1
+			;;
+	esac
+}
+
+setup_mount_prepared()
+{
+	setup_tgt=${1%/}
+	[ -n "$setup_part_root" ] || return 1
+	mkdir -p "$setup_tgt" || return 1
+	mount "$setup_part_root" "$setup_tgt" || return 1
+	if [ -n "$setup_part_efi" ]; then
+		mkdir -p "$setup_tgt/boot/efi"
+		mount "$setup_part_efi" "$setup_tgt/boot/efi" || return 1
+	fi
+	if [ -n "$setup_part_home" ]; then
+		mkdir -p "$setup_tgt/home"
+		mount "$setup_part_home" "$setup_tgt/home" || return 1
+	fi
+	if [ -n "$setup_part_swap" ]; then
+		swapon "$setup_part_swap" 2>/dev/null || :
+	fi
+	return 0
+}
+
+setup_format_and_mount()
+{
+	setup_tgt=${1%/}
+	[ -n "$setup_part_root" ] || return 1
+	mkdir -p "$setup_tgt" || return 1
+
+	if [ -n "$setup_part_efi" ]; then
+		mkfs.vfat -F 32 -n SPS_EFI "$setup_part_efi" || return 1
+	fi
+	if [ -n "$setup_part_swap" ]; then
+		mkswap -L sps-swap "$setup_part_swap" || return 1
+	fi
+	mkfs.ext4 -F -L sps-root "$setup_part_root" || return 1
+	if [ -n "$setup_part_home" ]; then
+		mkfs.ext4 -F -L sps-home "$setup_part_home" || return 1
+	fi
+
+	mount "$setup_part_root" "$setup_tgt" || return 1
+	if [ -n "$setup_part_efi" ]; then
+		mkdir -p "$setup_tgt/boot/efi"
+		mount "$setup_part_efi" "$setup_tgt/boot/efi" || return 1
+	fi
+	if [ -n "$setup_part_home" ]; then
+		mkdir -p "$setup_tgt/home"
+		mount "$setup_part_home" "$setup_tgt/home" || return 1
+	fi
+	if [ -n "$setup_part_swap" ]; then
+		swapon "$setup_part_swap" 2>/dev/null || :
+	fi
+	return 0
+}
+
+setup_uuid_of()
+{
+	setup_p=$1
+	[ -n "$setup_p" ] || return 0
+	if setup_part_path blkid >/dev/null; then
+		blkid -s UUID -o value "$setup_p" 2>/dev/null || :
+	fi
+}
+
+setup_write_fstab()
+{
+	setup_tgt=${1%/}
+	setup_fstab=$setup_tgt/etc/fstab
+	mkdir -p "$setup_tgt/etc" || return 1
+	{
+		printf '%s\n' '# /etc/fstab written by SPS setup. Review before the first boot.'
+		setup_u=$(setup_uuid_of "$setup_part_root")
+		if [ -n "$setup_u" ]; then
+			printf 'UUID=%s / ext4 defaults 1 1\n' "$setup_u"
+		elif [ -n "$setup_part_root" ]; then
+			printf '%s / ext4 defaults 1 1\n' "$setup_part_root"
+		fi
+		setup_u=$(setup_uuid_of "$setup_part_efi")
+		if [ -n "$setup_u" ]; then
+			printf 'UUID=%s /boot/efi vfat umask=0077 0 2\n' "$setup_u"
+		elif [ -n "$setup_part_efi" ]; then
+			printf '%s /boot/efi vfat umask=0077 0 2\n' "$setup_part_efi"
+		fi
+		setup_u=$(setup_uuid_of "$setup_part_home")
+		if [ -n "$setup_u" ]; then
+			printf 'UUID=%s /home ext4 defaults 1 2\n' "$setup_u"
+		elif [ -n "$setup_part_home" ]; then
+			printf '%s /home ext4 defaults 1 2\n' "$setup_part_home"
+		fi
+		setup_u=$(setup_uuid_of "$setup_part_swap")
+		if [ -n "$setup_u" ]; then
+			printf 'UUID=%s none swap defaults 0 0\n' "$setup_u"
+		elif [ -n "$setup_part_swap" ]; then
+			printf '%s none swap defaults 0 0\n' "$setup_part_swap"
+		fi
+	} >"$setup_fstab"
+}
+
+setup_apply_partition()
+{
+	# Destructive. Caller already confirmed.
+	setup_tgt=$1
+	case $setup_partition in
+		none|existing|'')
+			return 0
+			;;
+	esac
+	[ -n "$setup_disk" ] || return 1
+	setup_partition_tool_ok || return 1
+	case $setup_partition in
+		efi-root|efi-swap-root|efi-swap-root-home|bios-root)
+			setup_script=$setup_work/sfdisk.script
+			setup_write_guided_script "$setup_script" "$setup_partition" \
+				"${setup_swap_size:-8GiB}" || return 1
+			setup_wipe_disk "$setup_disk"
+			setup_sfdisk_apply "$setup_disk" "$setup_script" || return 1
+			# Let the kernel reread the partition table.
+			if setup_part_path partprobe >/dev/null; then
+				partprobe "$setup_disk" 2>/dev/null || :
+			fi
+			sleep 1
+			setup_assign_guided_parts "$setup_disk" "$setup_partition" || return 1
+			setup_format_and_mount "$setup_tgt" || return 1
+			;;
+		manage)
+			[ -n "$setup_part_root" ] || return 1
+			if [ "${setup_format:-no}" = yes ]; then
+				setup_format_and_mount "$setup_tgt" || return 1
+			else
+				setup_mount_prepared "$setup_tgt" || return 1
+			fi
+			;;
+		*)
+			return 1
+			;;
+	esac
+	return 0
+}
diff --git a/lib/setup/sets/drivers.set b/lib/setup/sets/drivers.set
new file mode 100644
index 0000000..06aa9df
--- /dev/null
+++ b/lib/setup/sets/drivers.set
@@ -0,0 +1,9 @@
+name        drivers
+title       Detected drivers
+description Mesa, nouveau, firmware, and related GPU/NIC helpers. Auto-detection also adds these.
+default     0
+package     mesa
+package     nouveau
+package     linux-firmware
+package     intel-ucode
+package     amd-ucode
diff --git a/lib/setup/sets/firmware.set b/lib/setup/sets/firmware.set
index 7ac51fd..93b9607 100644
--- a/lib/setup/sets/firmware.set
+++ b/lib/setup/sets/firmware.set
@@ -1,5 +1,5 @@
 name        firmware
 title       Firmware
 description linux-firmware blob collection. Needed for many NICs and GPUs.
-default     0
+default     1
 package     linux-firmware
diff --git a/man/mkiso.1 b/man/mkiso.1
index daedc93..92b0f99 100644
--- a/man/mkiso.1
+++ b/man/mkiso.1
@@ -1,6 +1,6 @@
 .TH MKISO 1 "2026-08-31" "SPS 1.0" "SPS Commands"
 .SH NAME
-mkiso \- build an SPS live TTY ISO
+mkiso \- build an SPS Linux live ISO (Splux)
 .SH SYNOPSIS
 .nf
 mkiso --output FILE.iso [options]
@@ -8,16 +8,20 @@ mkiso --layout DIR [options]
 .fi
 .SH DESCRIPTION
 .B mkiso
-builds a hybrid ISO that boots to a text console with
+builds a hybrid ISO for
+.B SPS Linux
+(informally Splux). The image boots with a real init as PID 1 and
 .BR setup (1)
-on PATH. Flash it to USB, boot, mount a destination filesystem on
-.IR /mnt ,
-and run
-.B setup .
+on PATH. Default
+.B --init
+is systemd. If the build host has no systemd binaries, busybox init
+respawns gettys on tty1 and ttyS0 instead of exec-ing a shell as PID 1.
+SPS the package system does not require these images.
 .PP
-The image does not partition disks and never runs
-.B grub-install
-on a hard disk.
+Flash it to USB, boot, and run
+.B setup .
+.B --session plasma
+copies a host Plasma/Wayland stack; NVIDIA live graphics use nouveau.
 .B --layout
 writes the live root only, for tests and inspection.
 .PP
@@ -34,6 +38,17 @@ Write a bootable ISO.
 .BI --layout " dir"
 Populate a live root and exit.
 .TP
+.BI --session " name"
+.B tty
+or
+.B plasma .
+.TP
+.BI --init " name"
+.B systemd
+or
+.B openrc .
+Default systemd.
+.TP
 .BI --busybox " file"
 Static busybox used for the initramfs and live
 .BR /bin/busybox .
diff --git a/man/setup.1 b/man/setup.1
index 48a76a5..d86c7e4 100644
--- a/man/setup.1
+++ b/man/setup.1
@@ -1,6 +1,6 @@
 .TH SETUP 1 "2026-08-31" "SPS 1.0" "SPS Commands"
 .SH NAME
-setup \- dialog-based SPS system installer
+setup \- dialog-based SPS Linux installer (Splux)
 .SH SYNOPSIS
 .nf
 setup [options]
@@ -10,7 +10,14 @@ setup --plan [options]
 .fi
 .SH DESCRIPTION
 .B setup
-installs an SPS system into a target root. Interactive use is a sequence of
+installs
+.B SPS Linux
+(informally Splux) into a target root. It is the distro installer, not a
+generic Linux From Scratch helper. SPS the package system can be used on
+other trees without running this program; partitioning, users, and boot
+layout on those trees are yours to decide.
+.PP
+Interactive use is a sequence of
 .BR dialog (1)
 screens in the same spirit as the Slackware and FreeBSD installers: profile,
 optional package sets, extra packages, locale, hostname, timezone, keymap,
@@ -24,14 +31,21 @@ or
 does not start
 .BR dialog (1).
 .PP
-The installer does not partition disks, format filesystems, or run
+The installer does not run
 .B grub-install
 unless
 .B --install-bootloader
-is given, and even then it refuses to guess a disk. Bootloader instructions
-are written to
-.I /root/sps-bootloader.txt
-in the target.
+is given 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.
+Live images prefer systemd. If the build host has no systemd binaries,
+busybox init runs instead and respawns gettys on tty1 and ttyS0.
 .SH PROFILES
 .TP
 .B minimal
@@ -120,6 +134,11 @@ First unprivileged account (uid 1000). The password is left locked; see
 .B --flatpak yes|no
 Install the Flatpak set and attempt to add Flathub after packages land.
 .TP
+.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.
+.TP
 .B --nvidia yes|no
 Install NVIDIA open kernel modules and utilities. A matching kernel is
 required; this installer does not build one.
diff --git a/tests/run b/tests/run
index 0c47769..a09670a 100755
--- a/tests/run
+++ b/tests/run
@@ -7,6 +7,12 @@ case $0 in
 esac
 test_dir=$(CDPATH= cd -- "$test_dir" 2>/dev/null && pwd)
 
+# A developer shell that has run src/sget exports SPS_* into the environment.
+# Drop those so `make check` does not read the host config by accident.
+unset SPS_ROOT SPS_CONFIG SPS_REPOS_CONFIG SPS_DB SPS_CACHE SPS_BUILD \
+	SPS_LIBDIR SPS_REPO_ROOT SPS_COMPRESSION SPS_ARCH SPS_PRESERVE \
+	SPS_MAKEJOBS 2>/dev/null || :
+
 status=0
 count=0
 for test_file in "$test_dir"/test_*.sh; do
diff --git a/tests/test_conflicts.sh b/tests/test_conflicts.sh
new file mode 100644
index 0000000..7f41c5f
--- /dev/null
+++ b/tests/test_conflicts.sh
@@ -0,0 +1,66 @@
+#!/bin/sh
+# sget refuses systemd/OpenRC-style recipe conflicts.
+
+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_conflicts: %s\n' "$*" >&2; exit 1; }
+
+tmp=$(mktemp -d "${TMPDIR:-/tmp}/sps-conflicts.XXXXXX") || fail mktemp
+trap 'rm -rf "$tmp"' 0 HUP INT TERM
+
+mkdir -p "$tmp/root" "$tmp/cache" "$tmp/db" "$tmp/build" \
+	"$tmp/repo/alpha" "$tmp/repo/beta" "$tmp/repo/ok"
+: >"$tmp/sps.conf"
+printf 'dir test %s 10\n' "$tmp/repo" >"$tmp/repos.conf"
+
+write_recipe()
+{
+	dir=$1
+	name=$2
+	shift 2
+	{
+		printf 'name %s\n' "$name"
+		printf '%s\n' 'version 1.0' 'release 1' "description $name"
+		for rec
+		do
+			printf '%s\n' "$rec"
+		done
+		printf '%s\n' 'install mkdir -p "$PKG/usr/share/sps-test"'
+		printf 'install printf %%s\\n %s >"$PKG/usr/share/sps-test/%s"\n' \
+			"$name" "$name"
+	} >"$dir/recipe"
+}
+
+write_recipe "$tmp/repo/alpha" alpha 'conflict beta'
+write_recipe "$tmp/repo/beta" beta 'conflict alpha'
+write_recipe "$tmp/repo/ok" ok
+
+SPS_ROOT=$tmp/root
+SPS_DB=$tmp/db
+SPS_CACHE=$tmp/cache
+SPS_BUILD=$tmp/build
+SPS_CONFIG=$tmp/sps.conf
+SPS_REPOS_CONFIG=$tmp/repos.conf
+export SPS_ROOT SPS_DB SPS_CACHE SPS_BUILD SPS_CONFIG SPS_REPOS_CONFIG
+export SPS_LIBDIR=$project_dir/lib
+export PATH=$project_dir/bin:$PATH
+
+src update >/dev/null
+
+show=$(src show alpha)
+case $show in
+	*'conflicts: beta'*) ;;
+	*) fail "src show did not list conflict: $show" ;;
+esac
+
+set +e
+sget install --plan alpha beta >/dev/null 2>"$tmp/err"
+st=$?
+set -e
+[ "$st" -ne 0 ] || fail 'sget install --plan alpha beta should fail'
+grep -q conflict "$tmp/err" || fail "error did not mention conflict: $(cat "$tmp/err")"
+
+sget install --plan alpha ok >/dev/null || fail 'alpha+ok should resolve'
+sget install --plan ok >/dev/null || fail 'ok should resolve'
diff --git a/tests/test_install.sh b/tests/test_install.sh
index 046cd4a..d3cf153 100755
--- a/tests/test_install.sh
+++ b/tests/test_install.sh
@@ -44,9 +44,12 @@ cp "$project_dir/examples/sps.conf" "$root/etc/sps/sps.conf"
 printf 'repo examples %s 100\n' "$project_dir/examples" >"$root/etc/sps/repos.conf"
 
 SPS_ROOT=$root
+SPS_CONFIG=$root/etc/sps/sps.conf
+SPS_REPOS_CONFIG=$root/etc/sps/repos.conf
 SPS_COMPRESSION=none
 PATH=$image/usr/bin:$PATH
-export SPS_ROOT SPS_COMPRESSION PATH
+export SPS_ROOT SPS_CONFIG SPS_REPOS_CONFIG SPS_COMPRESSION PATH
+unset SPS_LIBDIR SPS_DB SPS_CACHE SPS_BUILD SPS_REPO_ROOT
 
 "$image/usr/bin/src" update >/dev/null || fail 'installed src update failed'
 plan=$("$image/usr/bin/sget" install --plan hello-sps) ||
diff --git a/tests/test_mkiso.sh b/tests/test_mkiso.sh
index c36c714..ac20ce3 100755
--- a/tests/test_mkiso.sh
+++ b/tests/test_mkiso.sh
@@ -20,6 +20,14 @@ case $help in
 	*'--with-firmware'*) ;;
 	*) fail 'help missing --with-firmware' ;;
 esac
+case $help in
+	*'--init'*) ;;
+	*) fail 'help missing --init' ;;
+esac
+case $help in
+	*'--session'*) ;;
+	*) fail 'help missing --session' ;;
+esac
 
 tmp=$(mktemp -d "${TMPDIR:-/tmp}/sps-test-mkiso.XXXXXX") || fail mktemp
 trap 'rm -rf "$tmp"' 0 HUP INT TERM
@@ -40,8 +48,19 @@ fi
 [ -f "$layout/etc/sps/live" ] || fail 'live marker missing'
 [ -f "$layout/etc/motd" ] || fail 'motd missing'
 [ -x "$layout/sbin/init" ] || fail 'live init missing'
+[ -x "$layout/etc/sps/live-rc" ] || fail 'live-rc missing'
+[ -f "$layout/etc/inittab" ] || fail 'inittab missing'
+[ "$(cat "$layout/etc/sps/init")" = systemd ] || fail 'live default init is not systemd'
 [ -x "$layout/usr/bin/setup" ] || fail 'setup not installed into layout'
 grep -q 'setup' "$layout/etc/motd" || fail 'motd does not mention setup'
+# PID 1 must not be the old script that execs a console (exit 127 panic).
+if [ -f "$layout/sbin/init" ]; then
+	init_head=$(head -c 2 "$layout/sbin/init" 2>/dev/null || :)
+	if [ "$init_head" = '#!' ] && grep -q 'exec setsid' "$layout/sbin/init"
+	then
+		fail 'PID 1 still execs a console as init'
+	fi
+fi
 [ -f "$layout/etc/sps/repos.conf" ] || fail 'repos.conf missing'
 if [ -x "$layout/bin/busybox" ] && [ -L "$layout/bin/mkdir" ]; then
 	target=$(readlink "$layout/bin/mkdir")
diff --git a/tests/test_recipe_contract.sh b/tests/test_recipe_contract.sh
index 4fc62b0..bdf0b2f 100755
--- a/tests/test_recipe_contract.sh
+++ b/tests/test_recipe_contract.sh
@@ -90,7 +90,7 @@ EOF_VALID
 valid_record=$(parse_recipe "$tmp/valid/recipe") ||
     fail 'complete recipe was rejected by the index parser'
 printf '%s\n' "$valid_record" | awk -F '\t' '
-    NF == 12 && $1 == "contract-valid" && $2 == "1.0" &&
+    NF == 13 && $1 == "contract-valid" && $2 == "1.0" &&
     $3 == "1" && $4 == "any" { valid=1 }
     END { exit !valid }
 ' || fail 'complete recipe produced the wrong index identity'
diff --git a/tests/test_repository.sh b/tests/test_repository.sh
index 1b9b730..8f106e0 100755
--- a/tests/test_repository.sh
+++ b/tests/test_repository.sh
@@ -90,7 +90,7 @@ contains "$update_output" 'updated 3 repositories, 4 package records'
 
 index=$tmp/cache/indexes/packages.index
 [ -r "$index" ] || fail 'aggregate index was not created'
-awk -F '\t' 'NF != 12 { bad = 1 }
+awk -F '\t' 'NF != 12 && NF != 13 { bad = 1 }
              END { exit bad || NR != 4 }' "$index" ||
     fail 'aggregate index does not contain four valid records'
 
diff --git a/tests/test_setup.sh b/tests/test_setup.sh
index c9db617..24a7ac8 100755
--- a/tests/test_setup.sh
+++ b/tests/test_setup.sh
@@ -30,12 +30,20 @@ contains "$sets" 'storage'
 contains "$sets" 'wifi'
 contains "$sets" 'firmware'
 contains "$sets" 'bluetooth'
+contains "$sets" 'drivers'
 
 plan=$("$setup" --plan --target /mnt --profile minimal --disable qol-cli)
 contains "$plan" 'Profile:    minimal'
 contains "$plan" 'base-system'
-contains "$plan" 'dialog'
-contains "$plan" 'Locale:     C.UTF-8'
+contains "$plan" 'Init:       none'
+
+plan=$("$setup" --plan --target /mnt --profile minimal --init systemd --disable qol-cli)
+contains "$plan" 'Init:       systemd'
+contains "$plan" 'init-systemd'
+
+plan=$("$setup" --plan --target /mnt --profile minimal --init openrc --disable qol-cli)
+contains "$plan" 'Init:       openrc'
+contains "$plan" 'init-openrc'
 
 plan=$("$setup" --plan --target /mnt --profile plasma-desktop --user alex \
 	--hostname darkstar --timezone UTC --keymap us --nvidia yes \