Splux Linux

source-based Linux using SPS

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

82b32d4


setup: set user and root passwords; start live Plasma via kwin.

Date
Commit82b32d44305362f6936360b01276c7b0c0704e69
GitHubmirror

Parents

Stat

 Makefile                      |   5 +-
 bin/mkiso                     |  35 +++++--
 bin/mkpkg                     |  57 +++++++++--
 bin/setup                     | 233 +++++++++++++++++++++++++++++++++++++++---
 lib/mkiso/initrd-init         |  18 +++-
 lib/mkiso/live-bins           |   2 +
 lib/mkiso/live-plasma         |  54 +++++++++-
 lib/mkiso/live-plasma-session |  49 +++++++++
 lib/mkiso/live-rc             |  16 ++-
 lib/mkiso/plasma-bins         |   1 +
 lib/setup/services            |   2 +
 man/setup.1                   |  26 ++++-
 tests/test_mkiso.sh           |  27 +++++
 tests/test_setup.sh           |  29 ++++++
 14 files changed, 513 insertions(+), 41 deletions(-)

Diff

diff --git a/Makefile b/Makefile
index 87f5abe..c4e3591 100644
--- a/Makefile
+++ b/Makefile
@@ -30,7 +30,8 @@ lint:
 		$(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 lib/mkiso/udhcpc-script; do \
+		lib/mkiso/live-plasma lib/mkiso/live-plasma-session \
+		lib/mkiso/live-init lib/mkiso/udhcpc-script; do \
 		[ -f $$script ] || continue; \
 		$(POSIX_SHELL) -n $$script; \
 	done
@@ -173,7 +174,7 @@ install:
 	@set -e; for f in lib/mkiso/*; do \
 		[ -f "$$f" ] || continue; \
 		case $$f in \
-			*/live-init|*/initrd-init|*/live-rc|*/live-plasma|*/udhcpc-script) \
+			*/live-init|*/initrd-init|*/live-rc|*/live-plasma|*/live-plasma-session|*/udhcpc-script) \
 				install -m 0755 $$f "$(DESTDIR)$(LIBDIR)/mkiso/" ;; \
 			*) \
 				install -m 0644 $$f "$(DESTDIR)$(LIBDIR)/mkiso/" ;; \
diff --git a/bin/mkiso b/bin/mkiso
index f1390f7..f32cce3 100755
--- a/bin/mkiso
+++ b/bin/mkiso
@@ -540,9 +540,8 @@ mkiso_stamp_os_release()
 		>"$mkiso_live/etc/os-release"
 }
 
-# Overlay extra's Distro PNG and about-distro files after host Plasma trees
-# so KInfoCenter and plasma-welcome on the live disc match an installed
-# splux-base, not the builder's /etc/xdg.
+# Overlay extra's Distro PNG and a hardcoded plasma-welcome intro so the
+# live disc does not read /etc/os-release for the first Welcome paragraph.
 mkiso_stamp_distro_branding()
 {
 	mkiso_brand=
@@ -551,29 +550,34 @@ mkiso_stamp_distro_branding()
 	then
 		mkiso_brand=$mkiso_extra/base/splux-base/files
 	fi
-	[ -n "$mkiso_brand" ] || return 0
 
 	mkdir -p "$mkiso_live/usr/share/pixmaps" \
 		"$mkiso_live/usr/share/icons/hicolor/512x512/apps" \
 		"$mkiso_live/etc/xdg" \
 		"$mkiso_live/usr/share/plasma/plasma-welcome"
-	if [ -f "$mkiso_brand/sps.png" ]; then
+	if [ -n "$mkiso_brand" ] && [ -f "$mkiso_brand/sps.png" ]; then
 		cp "$mkiso_brand/sps.png" "$mkiso_live/usr/share/pixmaps/splux.png"
 		cp "$mkiso_brand/sps.png" "$mkiso_live/usr/share/pixmaps/sps.png"
 		cp "$mkiso_brand/sps.png" \
 			"$mkiso_live/usr/share/icons/hicolor/512x512/apps/splux.png"
 	fi
-	if [ -f "$mkiso_brand/kcm-about-distrorc" ]; then
+	if [ -n "$mkiso_brand" ] && [ -f "$mkiso_brand/kcm-about-distrorc" ]; then
 		cp "$mkiso_brand/kcm-about-distrorc" \
 			"$mkiso_live/etc/xdg/kcm-about-distrorc"
 	fi
-	if [ -f "$mkiso_brand/intro-customization.desktop" ]; then
-		cp "$mkiso_brand/intro-customization.desktop" \
-			"$mkiso_live/usr/share/plasma/plasma-welcome/intro-customization.desktop"
-	fi
-	if [ -f "$mkiso_brand/os-release" ]; then
+	if [ -n "$mkiso_brand" ] && [ -f "$mkiso_brand/os-release" ]; then
 		cp "$mkiso_brand/os-release" "$mkiso_live/etc/os-release"
 	fi
+	# Hardcoded. plasma-welcome prepends this Name; it must not fall
+	# back to PRETTY_NAME from os-release.
+	cat >"$mkiso_live/usr/share/plasma/plasma-welcome/intro-customization.desktop" <<'EOF'
+[Desktop Entry]
+Type=Application
+Name=Welcome to the SPS Linux operating system running KDE Plasma
+Comment=SPS Linux
+Icon=file:/usr/share/pixmaps/sps.png
+URL=https://splux.robertflexx.dev
+EOF
 }
 
 # Fail the ISO build rather than ship a disc that respawns getty with
@@ -914,6 +918,11 @@ Autolock=false
 LockOnResume=false
 LockOnStart=false
 Timeout=0
+EOF
+	mkdir -p "$mkiso_live/root/.config"
+	cat >"$mkiso_live/root/.config/startkderc" <<'EOF'
+[General]
+systemdBoot=false
 EOF
 	# Host /etc/dbus-1 may already contain system-local.conf. live-plasma
 	# used to skip the root-user override when that file existed, which
@@ -957,6 +966,10 @@ mkiso_install_pid1()
 		"$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"
+	if [ -f "$mkiso_libdir/live-plasma-session" ]; then
+		install -m 0755 "$mkiso_libdir/live-plasma-session" \
+			"$mkiso_live/etc/sps/live-plasma-session"
+	fi
 	printf '%s\n' "$mkiso_init" >"$mkiso_live/etc/sps/init"
 	printf '%s\n' "$mkiso_session" >"$mkiso_live/etc/sps/live-session"
 
diff --git a/bin/mkpkg b/bin/mkpkg
index 25c2085..4a80419 100755
--- a/bin/mkpkg
+++ b/bin/mkpkg
@@ -840,6 +840,19 @@ if [ -n "${SPS_ROOT:-}" ] && [ "$SPS_ROOT" != / ]; then
 		export CXX
 	fi
 
+	# Autoconf sources CONFIG_SITE before AC_CHECK_LIB. AM_PATH_ALSA
+	# (alsa-utils) only adds -L when --with-alsa-prefix is set; this
+	# covers packages that do not have that switch.
+	{
+		printf '%s\n' '# generated by mkpkg for SPS_ROOT'
+		printf ': "${CPPFLAGS=%s}"\n' "$target_cflags"
+		printf ': "${CFLAGS=%s}"\n' "$target_cflags"
+		printf ': "${CXXFLAGS=%s}"\n' "$target_cflags"
+		printf ': "${LDFLAGS=%s}"\n' "$target_ldflags"
+	} >"$build_dir/run/config.site"
+	CONFIG_SITE=$build_dir/run/config.site
+	export CONFIG_SITE
+
 	say "using $target_root for headers, libraries, and pkg-config"
 fi
 
@@ -849,14 +862,20 @@ run_phase()
 	phase_file=$build_dir/run/$phase_name.sh
 	[ -s "$phase_file" ] || return 0
 	say "running $phase_name phase"
-	if [ -n "$run_env" ]; then
-		if ! (CDPATH= cd "$WORK" && env $run_env sh -eu "$phase_file"); then
-			die_status 10 "$phase_name phase failed"
-		fi
-	else
-		if ! (CDPATH= cd "$WORK" && sh -eu "$phase_file"); then
-			die_status 10 "$phase_name phase failed"
+	if ! (
+		CDPATH=
+		cd "$WORK" || exit 1
+		if [ -n "$build_root" ]; then
+			PATH=${wrapper_bin:+$wrapper_bin:}$build_root/usr/bin:$build_root/bin:$PATH
+			SYSROOT=$build_root
+			export PATH SYSROOT
+		elif [ -n "$wrapper_bin" ]; then
+			PATH=$wrapper_bin:$PATH
+			export PATH
 		fi
+		sh -eu "$phase_file"
+	); then
+		die_status 10 "$phase_name phase failed"
 	fi
 }
 
@@ -866,6 +885,30 @@ run_phase configure
 run_phase build
 run_phase install
 
+# -lfoo needs libfoo.so. Some DESTDIR installs only ship libfoo.so.N, or
+# leave a GNU ld script whose GROUP path is /usr/lib64 on the live ISO.
+if [ -d "$stage_dir" ]; then
+	find "$stage_dir" \( -type f -o -type l \) -name 'lib*.so.[0-9]*' |
+		while IFS= read -r sofile || [ -n "$sofile" ]
+		do
+			[ -n "$sofile" ] || continue
+			soname=${sofile##*/}
+			so_dir=${sofile%/*}
+			base=${soname%%.so.*}.so
+			[ "$base" != "$soname" ] || continue
+			link=$so_dir/$base
+			if [ -f "$link" ] && [ ! -L "$link" ] &&
+				grep -qE 'GROUP|INPUT' "$link" 2>/dev/null
+			then
+				ln -sf "$soname" "$link"
+				continue
+			fi
+			if [ ! -e "$link" ] && [ ! -L "$link" ]; then
+				ln -sf "$soname" "$link"
+			fi
+		done
+fi
+
 if [ -e "$stage_dir/.SPS" ] || [ -L "$stage_dir/.SPS" ]; then
 	die_status 10 "install phase attempted to provide reserved .SPS metadata"
 fi
diff --git a/bin/setup b/bin/setup
index dac3583..b920e2a 100755
--- a/bin/setup
+++ b/bin/setup
@@ -53,6 +53,10 @@ options:
   --shell PATH              login shell for the first user
   --user NAME               first unprivileged user (uid 1000)
   --user-gecos TEXT
+  --user-password-file FILE hash the first line as the user password
+  --root-password-file FILE hash the first line as the root password
+  --user-password-hash HASH already-hashed user shadow field
+  --root-password-hash HASH already-hashed root shadow field
   --extra PKG[,PKG...]      individual packages in addition to sets
   --ssh yes|no              include the ssh set (OpenSSH)
   --printing yes|no         include the printing set (CUPS)
@@ -165,6 +169,8 @@ setup_locale=
 setup_shell=
 setup_user=
 setup_user_gecos=
+setup_user_password_hash=
+setup_root_password_hash=
 setup_extra=
 setup_ssh=
 setup_printing=
@@ -231,6 +237,109 @@ setup_append_csv()
 	eval "$setup_dest=\"\$(printf '%s' \"\$setup_old\" | sed 's/^ //')\""
 }
 
+setup_hash_password_stdin()
+{
+	if command -v openssl >/dev/null 2>&1; then
+		openssl passwd -6 -stdin
+		return $?
+	fi
+	if command -v python3 >/dev/null 2>&1; then
+		python3 -c 'import crypt,sys
+p=sys.stdin.read().rstrip("\n")
+print(crypt.crypt(p, crypt.mksalt(crypt.METHOD_SHA512)))'
+		return $?
+	fi
+	if command -v busybox >/dev/null 2>&1; then
+		busybox cryptpw -m sha512 "$(cat)"
+		return $?
+	fi
+	return 1
+}
+
+setup_hash_password_file()
+{
+	setup_pwf=$1
+	[ -f "$setup_pwf" ] && [ -r "$setup_pwf" ] ||
+		setup_fail "$SPS_EX_NOTFOUND" "cannot read password file: $setup_pwf"
+	setup_pw=$(sed -n '1p' "$setup_pwf")
+	[ -n "$setup_pw" ] ||
+		setup_fail "$SPS_EX_USAGE" "password file is empty: $setup_pwf"
+	printf '%s\n' "$setup_pw" | setup_hash_password_stdin
+}
+
+setup_shadow_set_hash()
+{
+	setup_sh_file=$1
+	setup_sh_user=$2
+	setup_sh_hash=$3
+	[ -f "$setup_sh_file" ] || return 0
+	[ -n "$setup_sh_hash" ] || return 0
+	setup_sh_tmp=$setup_sh_file.tmp
+	setup_u=$setup_sh_user setup_h=$setup_sh_hash awk -F: '
+		BEGIN { OFS = ":" }
+		$1 == ENVIRON["setup_u"] { $2 = ENVIRON["setup_h"] }
+		{ print }
+	' "$setup_sh_file" >"$setup_sh_tmp" || return 1
+	mv "$setup_sh_tmp" "$setup_sh_file"
+	chmod 0640 "$setup_sh_file" 2>/dev/null || chmod 0600 "$setup_sh_file" || :
+}
+
+setup_group_ensure()
+{
+	setup_gf=$1
+	setup_gn=$2
+	setup_gid=$3
+	[ -f "$setup_gf" ] || return 0
+	grep -q "^$setup_gn:" "$setup_gf" && return 0
+	printf '%s:x:%s:\n' "$setup_gn" "$setup_gid" >>"$setup_gf"
+}
+
+setup_group_add_user()
+{
+	setup_gf=$1
+	setup_gn=$2
+	setup_gu=$3
+	[ -f "$setup_gf" ] || return 0
+	grep -q "^$setup_gn:" "$setup_gf" || return 0
+	setup_g_tmp=$setup_gf.tmp
+	setup_g=$setup_gn setup_u=$setup_gu awk -F: '
+		BEGIN { OFS = ":" }
+		$1 == ENVIRON["setup_g"] {
+			if ($4 == "") $4 = ENVIRON["setup_u"]
+			else {
+				n = split($4, a, ",")
+				found = 0
+				for (i = 1; i <= n; i++) if (a[i] == ENVIRON["setup_u"]) found = 1
+				if (!found) $4 = $4 "," ENVIRON["setup_u"]
+			}
+		}
+		{ print }
+	' "$setup_gf" >"$setup_g_tmp" || return 1
+	mv "$setup_g_tmp" "$setup_gf"
+}
+
+setup_ask_password_pair()
+{
+	setup_pw_title=$1
+	setup_pw_prompt=$2
+	setup_p1=$(setup_dialog --title "$setup_pw_title" \
+		--passwordbox "$setup_pw_prompt" 9 50) ||
+		setup_fail "$SPS_EX_USAGE" "cancelled"
+	setup_p2=$(setup_dialog --title "$setup_pw_title" \
+		--passwordbox "Repeat the password." 9 50) ||
+		setup_fail "$SPS_EX_USAGE" "cancelled"
+	if [ "$setup_p1" != "$setup_p2" ]; then
+		setup_fail "$SPS_EX_USAGE" "passwords do not match"
+	fi
+	if [ -z "$setup_p1" ]; then
+		printf '%s\n' ''
+		return 0
+	fi
+	printf '%s\n' "$setup_p1" | setup_hash_password_stdin ||
+		setup_fail "$SPS_EX_PACKAGE" \
+			"cannot hash passwords (need openssl passwd, python3, or busybox cryptpw)"
+}
+
 while [ "$#" -gt 0 ]; do
 	case $1 in
 		-h|--help)
@@ -307,6 +416,32 @@ while [ "$#" -gt 0 ]; do
 			setup_user_gecos=$2
 			shift 2
 			;;
+		--user-password-file)
+			setup_need_value "$@"
+			setup_user_password_hash=$(setup_hash_password_file "$2") ||
+				setup_fail "$SPS_EX_PACKAGE" "cannot hash $2"
+			[ -n "$setup_user_password_hash" ] ||
+				setup_fail "$SPS_EX_USAGE" "user password hash was empty"
+			shift 2
+			;;
+		--root-password-file)
+			setup_need_value "$@"
+			setup_root_password_hash=$(setup_hash_password_file "$2") ||
+				setup_fail "$SPS_EX_PACKAGE" "cannot hash $2"
+			[ -n "$setup_root_password_hash" ] ||
+				setup_fail "$SPS_EX_USAGE" "root password hash was empty"
+			shift 2
+			;;
+		--user-password-hash)
+			setup_need_value "$@"
+			setup_user_password_hash=$2
+			shift 2
+			;;
+		--root-password-hash)
+			setup_need_value "$@"
+			setup_root_password_hash=$2
+			shift 2
+			;;
 		--flatpak)
 			setup_need_value "$@"
 			setup_flatpak=$2
@@ -588,6 +723,14 @@ setup_load_answers()
 			shell) [ -n "$setup_shell" ] || setup_shell=$setup_av ;;
 			user) [ -n "$setup_user" ] || setup_user=$setup_av ;;
 			user-gecos) [ -n "$setup_user_gecos" ] || setup_user_gecos=$setup_av ;;
+			user-password-hash)
+				[ -n "$setup_user_password_hash" ] ||
+					setup_user_password_hash=$setup_av
+				;;
+			root-password-hash)
+				[ -n "$setup_root_password_hash" ] ||
+					setup_root_password_hash=$setup_av
+				;;
 			extra) setup_append_csv setup_extra "$setup_av" "$setup_extra" ;;
 			ssh) [ -n "$setup_ssh" ] || setup_ssh=$setup_av ;;
 			printing) [ -n "$setup_printing" ] || setup_printing=$setup_av ;;
@@ -1060,6 +1203,15 @@ The live image and auto drivers use nouveau. This extra set is the proprietary/o
 			setup_fail "$SPS_EX_USAGE" "cancelled"
 	fi
 
+	if [ -n "$setup_user" ] && [ -z "$setup_user_password_hash" ]; then
+		setup_user_password_hash=$(setup_ask_password_pair "User password" \
+			"Password for $setup_user. Leave empty to lock the account until you run passwd after boot.")
+	fi
+	if [ -z "$setup_root_password_hash" ]; then
+		setup_root_password_hash=$(setup_ask_password_pair "Root password" \
+			"Password for root. Leave empty to keep root locked.")
+	fi
+
 	if [ -z "$setup_shell" ]; then
 		setup_sh_args=
 		if [ -f "$setup_dir/shells" ]; then
@@ -1425,6 +1577,16 @@ setup_summary()
 	printf 'Locale:     %s\n' "$setup_locale"
 	printf 'Shell:      %s\n' "$setup_shell"
 	printf 'User:       %s\n' "${setup_user:-<none>}"
+	if [ -n "$setup_user_password_hash" ]; then
+		printf 'User pw:    set\n'
+	elif [ -n "$setup_user" ]; then
+		printf 'User pw:    locked\n'
+	fi
+	if [ -n "$setup_root_password_hash" ]; then
+		printf 'Root pw:    set\n'
+	else
+		printf 'Root pw:    locked\n'
+	fi
 	printf 'Sets:       %s\n' "${setup_enable:-<none>}"
 	printf 'Extra:      %s\n' "${setup_extra:-<none>}"
 	printf 'Flatpak:    %s\n' "$setup_flatpak"
@@ -1528,21 +1690,33 @@ setup_apply_config()
 	# prefixes so it could index the copied trees before first boot.
 	setup_write_repos_conf ""
 
+	if [ ! -f "$setup_etc/passwd" ]; then
+		printf '%s\n' \
+			'root:x:0:0:root:/root:/bin/sh' \
+			'nobody:x:65534:65534:Unprivileged User:/:/usr/bin/nologin' \
+			>"$setup_etc/passwd"
+		printf '%s\n' 'root:x:0:' 'wheel:x:10:' 'audio:x:17:' \
+			'video:x:18:' 'nogroup:x:65534:' \
+			>"$setup_etc/group"
+		printf '%s\n' \
+			'root:!:0:0:99999:7:::' \
+			'nobody:!:0:0:99999:7:::' \
+			>"$setup_etc/shadow"
+	fi
+	setup_group_ensure "$setup_etc/group" wheel 10
+	setup_group_ensure "$setup_etc/group" audio 17
+	setup_group_ensure "$setup_etc/group" video 18
+	setup_group_ensure "$setup_etc/group" input 97
+	setup_group_ensure "$setup_etc/group" kvm 36
+	setup_group_ensure "$setup_etc/group" users 100
+	if [ -f "$setup_etc/shadow" ] && ! grep -q '^root:' "$setup_etc/shadow"
+	then
+		printf '%s\n' 'root:!:0:0:99999:7:::' >>"$setup_etc/shadow"
+	fi
+
 	if [ -n "$setup_user" ]; then
 		setup_home=/home/$setup_user
 		mkdir -p "$setup_root$setup_home"
-		if [ ! -f "$setup_etc/passwd" ]; then
-			printf '%s\n' \
-				'root:x:0:0:root:/root:/bin/sh' \
-				'nobody:x:65534:65534:Unprivileged User:/:/usr/bin/nologin' \
-				>"$setup_etc/passwd"
-			printf '%s\n' 'root:x:0:' 'nogroup:x:65534:' \
-				>"$setup_etc/group"
-			printf '%s\n' \
-				'root:!:0:0:99999:7:::' \
-				'nobody:!:0:0:99999:7:::' \
-				>"$setup_etc/shadow"
-		fi
 		if ! grep -q "^$setup_user:" "$setup_etc/passwd"; then
 			printf '%s:x:1000:1000:%s:%s:%s\n' \
 				"$setup_user" \
@@ -1558,12 +1732,43 @@ setup_apply_config()
 		   ! grep -q "^$setup_user:" "$setup_etc/shadow"; then
 			printf '%s:!:0:0:99999:7:::\n' "$setup_user" >>"$setup_etc/shadow"
 		fi
-		cat >"$setup_root/root/sps-user.txt" <<EOF
+		for setup_g in wheel audio video input kvm users
+		do
+			setup_group_add_user "$setup_etc/group" "$setup_g" "$setup_user"
+		done
+		chown -R 1000:1000 "$setup_root$setup_home" 2>/dev/null || :
+		chmod 0750 "$setup_root$setup_home" 2>/dev/null || :
+		setup_shadow_set_hash "$setup_etc/shadow" "$setup_user" \
+			"$setup_user_password_hash"
+		if [ -n "$setup_user_password_hash" ]; then
+			cat >"$setup_root/root/sps-user.txt" <<EOF
+The account '$setup_user' (uid 1000) was recorded in /etc/passwd.
+The password was set during setup. The account is in wheel, audio, video,
+input, kvm, and users when those groups exist.
+EOF
+		else
+			cat >"$setup_root/root/sps-user.txt" <<EOF
 The account '$setup_user' (uid 1000) was recorded in /etc/passwd.
 The password is locked. From a live root run:
 
     passwd $setup_user
 EOF
+		fi
+	fi
+	setup_shadow_set_hash "$setup_etc/shadow" root "$setup_root_password_hash"
+	if [ -n "$setup_root_password_hash" ]; then
+		mkdir -p "$setup_root/root"
+		if [ -f "$setup_root/root/sps-user.txt" ]; then
+			printf '%s\n' 'The root password was set during setup.' \
+				>>"$setup_root/root/sps-user.txt"
+		else
+			printf '%s\n' 'The root password was set during setup.' \
+				>"$setup_root/root/sps-user.txt"
+		fi
+	elif [ ! -f "$setup_root/root/sps-user.txt" ]; then
+		mkdir -p "$setup_root/root"
+		printf '%s\n' 'The root password is locked. Run passwd after boot.' \
+			>"$setup_root/root/sps-user.txt"
 	fi
 
 	cat >"$setup_root/root/sps-bootloader.txt" <<EOF
@@ -1807,7 +2012,7 @@ if [ "$setup_services" = yes ] &&
 then
 	case $setup_init in
 		systemd|openrc)
-			printf '%s: enabling services for init %s\n' \
+			printf '%s: enabling services after user configuration, init %s\n' \
 				"$sps_prog" "$setup_init" >&2
 			setup_svc_list=$(setup_service_enable_present \
 				"$setup_root" "$setup_init") || setup_svc_list=
diff --git a/lib/mkiso/initrd-init b/lib/mkiso/initrd-init
index ed7f968..4ff8478 100755
--- a/lib/mkiso/initrd-init
+++ b/lib/mkiso/initrd-init
@@ -45,11 +45,25 @@ 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_console \
 	sdhci sdhci-pci mmc_block \
-	drm drm_kms_helper drm_ttm_helper ttm virtio_dma_buf virtio_gpu \
-	bochs simpledrm
+	drm drm_kms_helper drm_shmem_helper drm_ttm_helper ttm virtio_dma_buf \
+	virtio_gpu bochs qxl vmwgfx
 do
 	"$bb" modprobe "$mod" 2>/dev/null || :
 done
+# simpledrm on a firmware framebuffer next to virtio_gpu is a black KWin
+# with an instant cursor. Only load it when no real KMS node appeared.
+n=0
+while [ "$n" -lt 5 ]
+do
+	if [ -e /dev/dri/card0 ] || [ -e /dev/dri/card1 ]; then
+		break
+	fi
+	n=$((n + 1))
+	"$bb" sleep 1
+done
+if [ ! -e /dev/dri/card0 ] && [ ! -e /dev/dri/card1 ]; then
+	"$bb" modprobe simpledrm 2>/dev/null || :
+fi
 
 found=
 i=0
diff --git a/lib/mkiso/live-bins b/lib/mkiso/live-bins
index 92b2375..6afe773 100644
--- a/lib/mkiso/live-bins
+++ b/lib/mkiso/live-bins
@@ -32,3 +32,5 @@
 /usr/sbin/partprobe
 /sbin/dhcpcd
 /usr/sbin/dhcpcd
+/usr/bin/openssl
+/bin/openssl
diff --git a/lib/mkiso/live-plasma b/lib/mkiso/live-plasma
index a5207b4..bcf066b 100644
--- a/lib/mkiso/live-plasma
+++ b/lib/mkiso/live-plasma
@@ -63,9 +63,16 @@ if [ ! -s /root/.config/baloofilerc ]; then
 	printf '%s\n' '[Basic Settings]' 'Indexing-Enabled=false' \
 		>/root/.config/baloofilerc
 fi
+# startplasma-wayland prefers systemd --user. The live disc is busybox
+# PID 1, so force the classic plasma_session path if we fall back to it.
+if [ ! -s /root/.config/startkderc ]; then
+	printf '%s\n' '[General]' 'systemdBoot=false' >/root/.config/startkderc
+fi
 if command -v fc-cache >/dev/null 2>&1; then
 	fc-cache >/dev/null 2>&1 || :
 fi
+export LIBSEAT_BACKEND=seatd
+export SEATD_SOCK=/run/seatd.sock
 
 # Live NVIDIA is nouveau. Keep the proprietary stack unloaded.
 modprobe nouveau 2>/dev/null || :
@@ -151,6 +158,33 @@ if [ ! -e /dev/dri/card0 ] && [ ! -e /dev/dri/card1 ]; then
 	printf '%s\n' "SPS: use tty2 (Ctrl-Alt-F2) for a shell."
 	ls /dev/dri 2>/dev/null || :
 fi
+# Prefer a real KMS device. simpledrm plus virtio_gpu leaves KWin on the
+# firmware framebuffer: black screen with a cursor that appears immediately.
+sps_drm=
+for sps_card in /sys/class/drm/card[0-9] /sys/class/drm/card[1-9][0-9]
+do
+	[ -e "$sps_card" ] || continue
+	sps_node=/dev/dri/${sps_card##*/}
+	[ -e "$sps_node" ] || continue
+	sps_drv=
+	if [ -L "$sps_card/device/driver" ]; then
+		sps_drv=$(readlink "$sps_card/device/driver")
+		sps_drv=${sps_drv##*/}
+	fi
+	case $sps_drv in
+		simple-framebuffer|simpledrm) continue ;;
+	esac
+	sps_drm=$sps_node
+	break
+done
+if [ -n "$sps_drm" ]; then
+	export KWIN_DRM_DEVICES=$sps_drm
+fi
+for sps_vt in /sys/class/vtconsole/vtcon*
+do
+	[ -w "$sps_vt/bind" ] || continue
+	printf '%s\n' 0 >"$sps_vt/bind" 2>/dev/null || :
+done
 if [ ! -e /usr/lib64/gbm/dri_gbm.so ] && [ ! -e /usr/lib/gbm/dri_gbm.so ]
 then
 	printf '%s\n' "SPS: Mesa GBM backend dri_gbm.so is missing; KWin cannot use DRM."
@@ -160,9 +194,25 @@ then
 	printf '%s\n' "SPS: libSPIRV-Tools.so is missing; Mesa GBM will fail to open DRI."
 fi
 
+# Give the user a console they can see if Wayland dies.
+chvt 1 2>/dev/null || :
+# startplasma-wayland starts kwin, then waits on kded/ksmserver before
+# plasmashell. That hang is a black compositor with an instant cursor.
+# Drive KWin ourselves and exec plasmashell as the session.
+if command -v kwin_wayland >/dev/null 2>&1 &&
+   [ -x /etc/sps/live-plasma-session ]
+then
+	if command -v dbus-run-session >/dev/null 2>&1; then
+		exec dbus-run-session -- kwin_wayland --drm --xwayland \
+			--no-lockscreen \
+			--exit-with-session /etc/sps/live-plasma-session \
+			>>/run/plasma.log 2>&1
+	fi
+	exec kwin_wayland --drm --xwayland --no-lockscreen \
+		--exit-with-session /etc/sps/live-plasma-session \
+		>>/run/plasma.log 2>&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 || :
 	# startplasma needs a *session* bus. The system bus above is not that.
 	if command -v dbus-run-session >/dev/null 2>&1; then
 		exec dbus-run-session -- startplasma-wayland \
diff --git a/lib/mkiso/live-plasma-session b/lib/mkiso/live-plasma-session
new file mode 100644
index 0000000..1d245c6
--- /dev/null
+++ b/lib/mkiso/live-plasma-session
@@ -0,0 +1,49 @@
+#!/bin/busybox sh
+# Session started by kwin_wayland --exit-with-session. When this script
+# (or plasmashell) exits, KWin exits and busybox init respawns live-plasma.
+
+PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+PATH=/usr/lib64/qt6/bin:/usr/lib/qt6/bin:$PATH
+export PATH
+export HOME=${HOME:-/root}
+export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/0}
+export XDG_SESSION_TYPE=wayland
+export XDG_SESSION_CLASS=user
+export XDG_SESSION_DESKTOP=KDE
+export XDG_CURRENT_DESKTOP=KDE
+export DESKTOP_SESSION=plasma
+
+if command -v kded6 >/dev/null 2>&1; then
+	kded6 >/run/kded6.log 2>&1 &
+fi
+if command -v kglobalacceld >/dev/null 2>&1; then
+	kglobalacceld >/run/kglobalaccel.log 2>&1 &
+elif [ -x /usr/libexec/kglobalacceld ]; then
+	/usr/libexec/kglobalacceld >/run/kglobalaccel.log 2>&1 &
+fi
+if [ -x /usr/libexec/kactivitymanagerd ]; then
+	/usr/libexec/kactivitymanagerd >/run/kactivity.log 2>&1 &
+fi
+
+if command -v plasma-welcome >/dev/null 2>&1; then
+	(
+		n=0
+		while [ "$n" -lt 20 ]; do
+			if [ -n "${WAYLAND_DISPLAY:-}" ] ||
+			   [ -S "$XDG_RUNTIME_DIR/wayland-0" ]
+			then
+				break
+			fi
+			n=$((n + 1))
+			sleep 1
+		done
+		sleep 3
+		plasma-welcome
+	) >/run/plasma-welcome.log 2>&1 &
+fi
+
+if command -v plasmashell >/dev/null 2>&1; then
+	exec plasmashell
+fi
+printf '%s\n' "SPS: plasmashell is not on this image."
+exec /bin/sh
diff --git a/lib/mkiso/live-rc b/lib/mkiso/live-rc
index 576a980..77e8449 100644
--- a/lib/mkiso/live-rc
+++ b/lib/mkiso/live-rc
@@ -69,11 +69,23 @@ fi
 # or cirrus. KWin needs a DRM node; nouveau/bochs alone is not enough.
 for mod in e1000 e1000e r8169 virtio_net virtio_blk virtio_pci virtio_scsi \
 	usbnet hid-generic usbhid atkbd \
-	drm drm_kms_helper drm_ttm_helper ttm virtio_dma_buf virtio_gpu \
-	nouveau bochs cirrus_qemu qxl vmwgfx i915 amdgpu
+	drm drm_kms_helper drm_shmem_helper drm_ttm_helper ttm virtio_dma_buf \
+	virtio_gpu nouveau bochs cirrus_qemu qxl vmwgfx i915 amdgpu
 do
 	modprobe "$mod" 2>/dev/null || :
 done
+n=0
+while [ "$n" -lt 5 ]
+do
+	if [ -e /dev/dri/card0 ] || [ -e /dev/dri/card1 ]; then
+		break
+	fi
+	n=$((n + 1))
+	sleep 1
+done
+if [ ! -e /dev/dri/card0 ] && [ ! -e /dev/dri/card1 ]; then
+	modprobe simpledrm 2>/dev/null || :
+fi
 
 # udev first so virtio_net and friends have device nodes before DHCP.
 if command -v udevd >/dev/null 2>&1; then
diff --git a/lib/mkiso/plasma-bins b/lib/mkiso/plasma-bins
index a5f09fe..412ab0c 100644
--- a/lib/mkiso/plasma-bins
+++ b/lib/mkiso/plasma-bins
@@ -11,6 +11,7 @@
 /usr/libexec/kwin_killer_helper
 /usr/bin/plasmashell
 /usr/bin/plasma_session
+/usr/bin/plasma-welcome
 /usr/bin/plasma_waitforname
 /usr/bin/plasma-shutdown
 /usr/bin/ksplashqml
diff --git a/lib/setup/services b/lib/setup/services
index 2a2b96f..333d263 100644
--- a/lib/setup/services
+++ b/lib/setup/services
@@ -15,3 +15,5 @@ bluetooth	bluetooth.service	multi-user.target	bluetooth	default
 sshd	sshd.service	multi-user.target	sshd	default
 cupsd	cups.service	multi-user.target	cupsd	default
 NetworkManager	NetworkManager.service	multi-user.target	NetworkManager	default
+pipewire	pipewire.service	default.target	pipewire	default
+wireplumber	wireplumber.service	default.target	wireplumber	default
diff --git a/man/setup.1 b/man/setup.1
index b4a0f78..ad95db7 100644
--- a/man/setup.1
+++ b/man/setup.1
@@ -158,7 +158,31 @@ Enable the OpenSSH set.
 Enable CUPS and related printing packages.
 .TP
 .BI --user " name"
-First unprivileged account (uid 1000). The password is left locked; see
+First unprivileged account (uid 1000). Interactive setup asks for a
+user password and a root password (empty keeps the account locked).
+Scripts should pass a file or an already-hashed shadow field; plaintext
+passwords are not accepted on the command line.
+.TP
+.BI --user-password-file " file"
+.TP
+.BI --root-password-file " file"
+Hash the first line of
+.I file
+with
+.B openssl passwd -6
+(or python3 crypt / busybox cryptpw) and store it in shadow.
+.TP
+.BI --user-password-hash " hash"
+.TP
+.BI --root-password-hash " hash"
+Already-hashed shadow field. Answers files may use
+.B user-password-hash
+and
+.B root-password-hash
+the same way. Those hashes are not written back to
+.I /etc/sps/setup-answers .
+Without a password flag, non-interactive setup leaves the account locked;
+see
 .I /root/sps-user.txt .
 .TP
 .B --flatpak yes|no
diff --git a/tests/test_mkiso.sh b/tests/test_mkiso.sh
index 10311e6..0f36e58 100755
--- a/tests/test_mkiso.sh
+++ b/tests/test_mkiso.sh
@@ -102,6 +102,24 @@ grep -q 'Engine=none' "$layout/etc/sps/live-plasma" ||
 	fail 'live-plasma must disable KSplash so plasma_waitforname cannot hang'
 grep -q LockOnStart "$layout/etc/sps/live-plasma" ||
 	fail 'live-plasma must disable kscreenlocker LockOnStart'
+grep -q 'kwin_wayland --drm' "$layout/etc/sps/live-plasma" ||
+	fail 'live-plasma must drive kwin_wayland --drm'
+[ -x "$layout/etc/sps/live-plasma-session" ] ||
+	fail 'live-plasma-session missing from layout'
+grep -q LIBSEAT_BACKEND=seatd "$layout/etc/sps/live-plasma" ||
+	fail 'live-plasma must set LIBSEAT_BACKEND=seatd'
+welcome=$layout/usr/share/plasma/plasma-welcome/intro-customization.desktop
+[ -f "$welcome" ] || fail 'plasma-welcome intro-customization missing'
+grep -qx 'Name=Welcome to the SPS Linux operating system running KDE Plasma' \
+	"$welcome" || fail 'plasma-welcome Name must be the SPS Linux sentence'
+name_line=$(grep '^Name=' "$welcome")
+case $name_line in
+	*[Ss]plux*) fail 'plasma-welcome Name must not fall back to os-release Splux' ;;
+esac
+grep -q plasma-welcome "$project_dir/lib/mkiso/plasma-bins" ||
+	fail 'plasma-bins must include plasma-welcome'
+grep -q openssl "$project_dir/lib/mkiso/live-bins" ||
+	fail 'live-bins must include openssl for setup password hashes'
 grep -q virtio_gpu "$project_dir/lib/mkiso/initrd-init" ||
 	fail 'initrd must load virtio_gpu so Plasma has DRM before switch_root'
 if grep -q 'export QT_QPA_PLATFORM=wayland' "$layout/etc/sps/live-plasma"
@@ -114,6 +132,13 @@ grep -q virtio_gpu "$layout/etc/sps/live-rc" ||
 	fail 'live-rc must load virtio_gpu for QEMU/virt-manager'
 grep -q cirrus_qemu "$layout/etc/sps/live-rc" ||
 	fail 'live-rc must load cirrus_qemu for QEMU stdvga fallbacks'
+# simpledrm only after a wait; loading it beside virtio_gpu blacks KWin.
+live_rc=$layout/etc/sps/live-rc
+wait_line=$(grep -n 'sleep 1' "$live_rc" | head -1 | cut -d: -f1)
+sdrm_line=$(grep -n 'modprobe simpledrm' "$live_rc" | head -1 | cut -d: -f1)
+[ -n "$wait_line" ] && [ -n "$sdrm_line" ] &&
+	[ "$wait_line" -lt "$sdrm_line" ] ||
+	fail 'live-rc must wait for a DRM node before simpledrm'
 grep -q /dev/dri/card0 "$layout/etc/sps/live-plasma" ||
 	fail 'live-plasma must wait for a DRM node'
 grep -q /usr/share/kwin "$project_dir/lib/mkiso/plasma-trees" ||
@@ -252,6 +277,8 @@ grep -q 'using .* for headers, libraries, and pkg-config' "$project_dir/bin/mkpk
 	fail 'mkpkg must compile against SPS_ROOT so setup can link target libs'
 grep -q write_cc_wrapper "$project_dir/bin/mkpkg" ||
 	fail 'mkpkg must wrap gcc so autoconf finds libraries in SPS_ROOT'
+grep -q CONFIG_SITE "$project_dir/bin/mkpkg" ||
+	fail 'mkpkg must export CONFIG_SITE so autoconf sees SPS_ROOT LDFLAGS'
 grep -q 'sps_say "unpacking' "$project_dir/bin/pkin" ||
 	fail 'pkin must log unpacking'
 grep -q sps_verbose_say "$project_dir/bin/pkin" ||
diff --git a/tests/test_setup.sh b/tests/test_setup.sh
index a433783..a7d8607 100755
--- a/tests/test_setup.sh
+++ b/tests/test_setup.sh
@@ -145,6 +145,13 @@ src update >/dev/null
 [ "$(cat "$root/etc/locale.conf")" = 'LANG=C.UTF-8' ] || fail 'locale not written'
 grep -q '^tester:x:1000:1000:Test User:/home/tester:/bin/sh$' \
 	"$root/etc/passwd" || fail 'user not recorded'
+awk -F: '$1=="tester" && $2=="!" { found=1 } END { exit !found }' \
+	"$root/etc/shadow" || fail 'user without password flags must stay locked'
+awk -F: '$1=="root" && $2=="!" { found=1 } END { exit !found }' \
+	"$root/etc/shadow" || fail 'root without password flags must stay locked'
+if grep -q 'password-hash' "$root/etc/sps/setup-answers"; then
+	fail 'setup-answers must not store password hashes'
+fi
 [ -f "$root/etc/sps/setup-answers" ] || fail 'setup-answers missing'
 [ -f "$root/root/sps-bootloader.txt" ] || fail 'bootloader notes missing'
 [ -f "$root/root/sps-network.txt" ] || fail 'network notes missing'
@@ -155,6 +162,28 @@ grep -q 'Init is none' "$root/root/sps-services.txt" ||
 grep -q 'grub-install' "$root/root/sps-bootloader.txt" ||
 	fail 'bootloader notes omitted grub-install warning'
 
+printf '%s\n' 'test-user-pw' >"$tmp/user.pw"
+printf '%s\n' 'test-root-pw' >"$tmp/root.pw"
+chmod 600 "$tmp/user.pw" "$tmp/root.pw"
+root_pw=$tmp/root-pw
+mkdir -p "$root_pw"
+"$setup" --non-interactive --no-update --target "$root_pw" --profile tiny \
+	--hostname pwtest --timezone UTC --keymap us --user tester \
+	--enable extra --user-gecos 'PW User' \
+	--user-password-file "$tmp/user.pw" \
+	--root-password-file "$tmp/root.pw" >/dev/null
+awk -F: '$1=="tester" && $2!="" && $2!="!" && $2!="*" { found=1 }
+	END { exit !found }' "$root_pw/etc/shadow" ||
+	fail 'user password file did not update shadow'
+awk -F: '$1=="root" && $2!="" && $2!="!" && $2!="*" { found=1 }
+	END { exit !found }' "$root_pw/etc/shadow" ||
+	fail 'root password file did not update shadow'
+if grep -q 'password-hash' "$root_pw/etc/sps/setup-answers"; then
+	fail 'setup-answers must not store password hashes'
+fi
+grep -q 'wheel:.*tester' "$root_pw/etc/group" ||
+	fail 'first user should be in wheel when that group exists'
+
 printf '%s\n' 'setup fixture tests passed'
 
 # Bundled live recipes: copy into the target and index locally (no Git clone).