Splux Linux

source-based Linux using SPS

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

5571ccb


setup: compile against the target and only offer packaged extras

Date
Commit5571ccbb1bf109b26facec27e06e5c87d1663f4e
GitHubmirror

Parents

Stat

 bin/mkiso                   | 156 ++++++++++++++++++++++++++++++++++++++++++--
 bin/mkpkg                   |  53 +++++++++++++++
 bin/setup                   |  61 +++++++++++++++++
 lib/deps.awk                |  18 ++---
 lib/mkiso/live-plasma       |  21 +++++-
 lib/mkiso/plasma-bins       |   2 +
 lib/mkiso/plasma-trees      |   2 +
 lib/setup/extras            | 140 +++++++++++++++++++--------------------
 man/mkpkg.1                 |  17 +++++
 man/setup.1                 |   4 +-
 man/sget.1                  |   9 +++
 tests/test_build.sh         |  48 ++++++++++++++
 tests/test_dependencies.sh  |   8 +++
 tests/test_mkiso.sh         |  32 +++++++++
 tests/test_setup_catalog.sh | 120 ++++++++++++++++++++++++++++++++++
 15 files changed, 596 insertions(+), 95 deletions(-)

Diff

diff --git a/bin/mkiso b/bin/mkiso
index c6d1e15..7323644 100755
--- a/bin/mkiso
+++ b/bin/mkiso
@@ -352,6 +352,29 @@ mkiso_copy_mesa_deps()
 		"$mkiso_live/usr/lib/gbm/nvidia-drm_gbm.so"
 }
 
+# Session helpers and plugins plasmashell loads by soname. Tree copies can
+# leave the .so files in place while ldd of plasma-bins never saw them.
+mkiso_copy_plasma_runtime_deps()
+{
+	printf '%s: resolving Plasma session libraries\n' "$sps_prog" >&2
+	for mkiso_so in \
+		/usr/libexec/kactivitymanagerd \
+		/usr/libexec/kscreenlocker_greet \
+		/usr/bin/plasmashell \
+		/usr/bin/plasma_session \
+		/usr/bin/kwin_wayland \
+		/usr/bin/startplasma-wayland \
+		/usr/lib64/libseat.so \
+		/usr/lib64/libseat.so.1 \
+		/usr/lib64/qt6/plugins/wayland-shell-integration/liblayer-shell.so \
+		/usr/lib64/qt6/plugins/styles/breeze6.so \
+		/usr/lib64/qt6/plugins/platforms/libqwayland.so
+	do
+		[ -e "$mkiso_so" ] || [ -L "$mkiso_so" ] || continue
+		mkiso_copy_so_deps "$mkiso_so"
+	done
+}
+
 # ldd a host ELF and require each resolved library to exist in the live
 # root. Used so we refuse to ship gcc without libmpc or Mesa without SPIRV.
 mkiso_verify_elf_in_live()
@@ -617,6 +640,7 @@ mkiso_verify_live()
 
 	mkiso_verify_compiler
 	mkiso_verify_plasma
+	mkiso_verify_setup_catalog
 }
 
 # Seeded images ship gcc. The driver DT_NEEDs only libc; cc1 needs
@@ -674,8 +698,10 @@ mkiso_verify_plasma()
 	[ "$mkiso_session" = plasma ] || return 0
 	mkiso_miss=
 	for mkiso_need in usr/bin/kwin_wayland usr/bin/kwin_wayland_wrapper \
-		usr/bin/startplasma-wayland usr/lib64/gbm/dri_gbm.so \
-		usr/lib64/libxcb-cursor.so.0
+		usr/bin/startplasma-wayland usr/bin/plasmashell \
+		usr/libexec/kactivitymanagerd usr/libexec/kscreenlocker_greet \
+		usr/lib64/gbm/dri_gbm.so usr/lib64/libxcb-cursor.so.0 \
+		etc/xdg/kscreenlockerrc
 	do
 		if [ ! -e "$mkiso_live/$mkiso_need" ] &&
 			[ ! -L "$mkiso_live/$mkiso_need" ]
@@ -701,6 +727,10 @@ mkiso_verify_plasma()
 	then
 		mkiso_miss="$mkiso_miss usr/bin/Xwayland"
 	fi
+	if ! ls "$mkiso_live"/usr/share/fonts/TTF/*.ttf >/dev/null 2>&1
+	then
+		mkiso_miss="$mkiso_miss usr/share/fonts/TTF"
+	fi
 	[ -z "$mkiso_miss" ] ||
 		sps_die "$SPS_EX_PACKAGE" "plasma image missing:$mkiso_miss"
 	for mkiso_g in /usr/lib64/libgallium*.so
@@ -711,6 +741,103 @@ mkiso_verify_plasma()
 	done
 }
 
+# setup extras/sets/profiles may only name packages that exist in the
+# recipe trees on this image. An extras checklist entry for mpv with no
+# mpv recipe is how live setup died at "package 'mpv' not found".
+mkiso_verify_setup_catalog()
+{
+	mkiso_setupdir=$SPS_LIBDIR/setup
+	mkiso_extras=$mkiso_setupdir/extras
+	[ -f "$mkiso_extras" ] ||
+		sps_die "$SPS_EX_PACKAGE" "setup extras file is missing"
+	LC_ALL=C awk '
+		BEGIN { bad = 0; count = 0 }
+		/^[[:space:]]*$/ || /^#/ { next }
+		{
+			tab = index($0, "\t")
+			if (tab == 0) {
+				printf "mkiso: extras line has no tab: %s\n", $0
+				bad = 1
+				next
+			}
+			name = substr($0, 1, tab - 1)
+			if (name !~ /^[A-Za-z0-9][A-Za-z0-9+_.-]*$/) {
+				printf "mkiso: extras bad name: %s\n", name
+				bad = 1
+			}
+			if (name in seen) {
+				printf "mkiso: extras duplicate: %s\n", name
+				bad = 1
+			}
+			seen[name] = 1
+			count++
+			names[count] = name
+		}
+		END {
+			for (i = 1; i < count; i++)
+				if (names[i] > names[i + 1]) {
+					printf "mkiso: extras not sorted: %s before %s\n", \
+						names[i], names[i + 1]
+					bad = 1
+				}
+			exit bad
+		}
+	' "$mkiso_extras" ||
+		sps_die "$SPS_EX_PACKAGE" \
+			"setup extras must be unique, tab-separated, and sorted"
+
+	mkiso_recipes=$mkiso_work/catalog-recipes
+	: >"$mkiso_recipes"
+	for mkiso_tree in \
+		"$mkiso_live/usr/src/sps/core" \
+		"$mkiso_live/usr/src/sps/extra"
+	do
+		[ -d "$mkiso_tree" ] || continue
+		find "$mkiso_tree" \( -type d \( -name .git -o -name files -o \
+			-name patches -o -name hooks \) -prune \) -o \
+			-type f -name recipe -print
+	done | while IFS= read -r mkiso_rfile || [ -n "$mkiso_rfile" ]
+	do
+		[ -n "$mkiso_rfile" ] || continue
+		awk '/^name[[:space:]]/ { print $2; exit }' "$mkiso_rfile"
+	done | LC_ALL=C sort -u >"$mkiso_recipes"
+	[ -s "$mkiso_recipes" ] || return 0
+
+	mkiso_wanted=$mkiso_work/catalog-wanted
+	: >"$mkiso_wanted"
+	awk -F '\t' '/^[[:space:]]*$/ || /^#/ { next } { print $1 }' \
+		"$mkiso_extras" >>"$mkiso_wanted"
+	for mkiso_f in "$mkiso_setupdir"/sets/*.set \
+		"$mkiso_setupdir"/profiles/*.profile
+	do
+		[ -f "$mkiso_f" ] || continue
+		awk '/^package[[:space:]]/ { print $2 }' "$mkiso_f" \
+			>>"$mkiso_wanted"
+	done
+	if [ -f "$mkiso_setupdir/hwdetect.sh" ]; then
+		awk '
+			/printf '\''%s\\n'\'' [a-z]/ && $0 !~ /\$/ && $0 !~ /\|/ {
+				for (i = 3; i <= NF; i++) {
+					gsub(/;/, "", $i)
+					if ($i ~ /^[a-z][a-z0-9+-]*$/)
+						print $i
+				}
+			}
+		' "$mkiso_setupdir/hwdetect.sh" >>"$mkiso_wanted"
+	fi
+	LC_ALL=C sort -u "$mkiso_wanted" -o "$mkiso_wanted"
+	mkiso_miss=
+	while IFS= read -r mkiso_pkg || [ -n "$mkiso_pkg" ]
+	do
+		[ -n "$mkiso_pkg" ] || continue
+		grep -qx "$mkiso_pkg" "$mkiso_recipes" && continue
+		mkiso_miss="$mkiso_miss $mkiso_pkg"
+	done <"$mkiso_wanted"
+	[ -z "$mkiso_miss" ] ||
+		sps_die "$SPS_EX_PACKAGE" \
+			"setup names packages with no recipe:$mkiso_miss"
+}
+
 # BusyBox --install -s writes absolute symlinks to the host path. Relative
 # links keep applets working inside the squashfs and initramfs.
 mkiso_busybox_links()
@@ -772,13 +899,21 @@ mkiso_copy_plasma()
 			mkiso_copy_path "$mkiso_so"
 		done
 	done
-	# Full /usr/share/fonts/TTF is hundreds of megabytes. DejaVu is enough
-	# for fontconfig's default aliases on the live session.
-	for mkiso_font in /usr/share/fonts/TTF/DejaVu*.ttf
+	# Host fontconfig prefers Liberation Sans. DejaVu alone leaves
+	# fc-match pointing at a file that is not on the disc.
+	for mkiso_font in /usr/share/fonts/TTF/DejaVu*.ttf \
+		/usr/share/fonts/TTF/Liberation*.ttf
 	do
 		[ -e "$mkiso_font" ] || continue
 		mkiso_copy_path "$mkiso_font"
 	done
+	mkdir -p "$mkiso_live/etc/xdg"
+	cat >"$mkiso_live/etc/xdg/kscreenlockerrc" <<'EOF'
+[Daemon]
+Autolock=false
+LockOnResume=false
+Timeout=0
+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
 	# left dbus-daemon --system looking for a messagebus account the live
@@ -806,6 +941,7 @@ EOF
 	mkdir -p "$mkiso_live/etc/systemd/system"
 	ln -sf /dev/null "$mkiso_live/etc/systemd/system/dbus.service"
 	ln -sf /dev/null "$mkiso_live/etc/systemd/system/dbus.socket"
+	mkiso_copy_plasma_runtime_deps
 	mkiso_copy_mesa_deps
 }
 
@@ -1096,6 +1232,16 @@ if [ "$mkiso_copy_seed" -eq 1 ] && [ -f "$mkiso_libdir/seed-bins" ]; then
 	done
 	# cc1 DT_NEEDs libmpc/libisl. ldd of the gcc driver does not.
 	mkiso_copy_gcc_deps
+	# file(1) is in seed-bins. Autoconf calls it during configure, and
+	# without the magic database it prints "could not find any valid
+	# magic files" and some probes then mis-detect object types.
+	printf '%s: copying file(1) magic database\n' "$sps_prog" >&2
+	for mkiso_magic in /etc/file /usr/share/file /usr/share/misc/magic \
+		/usr/share/misc/magic.mgc /usr/share/file/magic.mgc
+	do
+		[ -e "$mkiso_magic" ] || [ -L "$mkiso_magic" ] || continue
+		mkiso_copy_path "$mkiso_magic"
+	done
 	for mkiso_cmd in gcc g++ cc c++ as ld ld.bfd
 	do
 		mkiso_copy_host_cmd "$mkiso_cmd" || :
diff --git a/bin/mkpkg b/bin/mkpkg
index 53aa62e..93f0af1 100755
--- a/bin/mkpkg
+++ b/bin/mkpkg
@@ -718,6 +718,59 @@ else
 	run_env=
 fi
 
+# Live setup builds into SPS_ROOT (a mounted disk) with the live compiler.
+# gcc and pkg-config search /usr unless they are pointed at packages pkin
+# already unpacked into the target. That is why alsa-utils configure found
+# host ALSA headers then died with "No linkable libasound was found" after
+# alsa-lib had been installed into the target, not the live root. The same
+# hole hits every later package that links a library from the target.
+if [ -n "${SPS_ROOT:-}" ] && [ "$SPS_ROOT" != / ]; then
+	target_root=$SPS_ROOT
+	target_inc=$target_root/usr/include
+	target_pc=$target_root/usr/lib64/pkgconfig
+	target_pc=$target_pc:$target_root/usr/lib/pkgconfig
+	target_pc=$target_pc:$target_root/usr/share/pkgconfig
+	target_pc=$target_pc:$target_root/lib64/pkgconfig
+	target_pc=$target_pc:$target_root/lib/pkgconfig
+	target_libpath=$target_root/usr/lib64:$target_root/usr/lib
+	target_libpath=$target_libpath:$target_root/lib64:$target_root/lib
+	target_cflags="-I$target_inc"
+	target_ldflags="-L$target_root/usr/lib64 -L$target_root/usr/lib"
+	target_ldflags="$target_ldflags -L$target_root/lib64 -L$target_root/lib"
+
+	PKG_CONFIG_SYSROOT_DIR=$target_root
+	PKG_CONFIG_LIBDIR=$target_pc
+	PKG_CONFIG_PATH=$target_pc${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}
+	PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1
+	PKG_CONFIG_ALLOW_SYSTEM_LIBS=1
+	export PKG_CONFIG_SYSROOT_DIR PKG_CONFIG_LIBDIR PKG_CONFIG_PATH \
+		PKG_CONFIG_ALLOW_SYSTEM_CFLAGS PKG_CONFIG_ALLOW_SYSTEM_LIBS
+
+	CPATH=$target_inc${CPATH:+:$CPATH}
+	C_INCLUDE_PATH=$target_inc${C_INCLUDE_PATH:+:$C_INCLUDE_PATH}
+	CPLUS_INCLUDE_PATH=$target_inc${CPLUS_INCLUDE_PATH:+:$CPLUS_INCLUDE_PATH}
+	LIBRARY_PATH=$target_libpath${LIBRARY_PATH:+:$LIBRARY_PATH}
+	LD_LIBRARY_PATH=$target_libpath${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
+	export CPATH C_INCLUDE_PATH CPLUS_INCLUDE_PATH LIBRARY_PATH \
+		LD_LIBRARY_PATH
+
+	CPPFLAGS="$target_cflags${CPPFLAGS:+ $CPPFLAGS}"
+	CFLAGS="$target_cflags${CFLAGS:+ $CFLAGS}"
+	CXXFLAGS="$target_cflags${CXXFLAGS:+ $CXXFLAGS}"
+	LDFLAGS="$target_ldflags${LDFLAGS:+ $LDFLAGS}"
+	export CPPFLAGS CFLAGS CXXFLAGS LDFLAGS
+
+	CMAKE_PREFIX_PATH=$target_root/usr${CMAKE_PREFIX_PATH:+:$CMAKE_PREFIX_PATH}
+	CMAKE_INCLUDE_PATH=$target_inc${CMAKE_INCLUDE_PATH:+:$CMAKE_INCLUDE_PATH}
+	CMAKE_LIBRARY_PATH=$target_libpath${CMAKE_LIBRARY_PATH:+:$CMAKE_LIBRARY_PATH}
+	export CMAKE_PREFIX_PATH CMAKE_INCLUDE_PATH CMAKE_LIBRARY_PATH
+
+	ACLOCAL_PATH=$target_root/usr/share/aclocal${ACLOCAL_PATH:+:$ACLOCAL_PATH}
+	export ACLOCAL_PATH
+
+	say "using $target_root for headers, libraries, and pkg-config"
+fi
+
 run_phase()
 {
 	phase_name=$1
diff --git a/bin/setup b/bin/setup
index dcf25c8..dac3583 100755
--- a/bin/setup
+++ b/bin/setup
@@ -100,6 +100,55 @@ if [ -f "$SPS_LIBDIR/setup/service.sh" ]; then
 	# shellcheck disable=SC1091
 	. "$SPS_LIBDIR/setup/service.sh"
 fi
+
+setup_collect_recipe_names()
+{
+	setup_names_out=$1
+	: >"$setup_names_out"
+	for setup_rtree in \
+		/usr/src/sps/core \
+		/usr/src/sps/extra \
+		"${SPS_LIVE_ROOT:-/}/usr/src/sps/core" \
+		"${SPS_LIVE_ROOT:-/}/usr/src/sps/extra"
+	do
+		[ -d "$setup_rtree" ] || continue
+		find "$setup_rtree" \( -type d \( -name .git -o -name files -o \
+			-name patches -o -name hooks \) -prune \) -o \
+			-type f -name recipe -print
+	done | while IFS= read -r setup_rfile || [ -n "$setup_rfile" ]
+	do
+		[ -n "$setup_rfile" ] || continue
+		awk '/^name[[:space:]]/ { print $2; exit }' "$setup_rfile"
+	done | LC_ALL=C sort -u >"$setup_names_out"
+}
+
+setup_reject_unknown_packages()
+{
+	setup_index=${SPS_CACHE:-$setup_root/var/cache/sps}/indexes/packages.index
+	[ -f "$setup_index" ] || return 0
+	[ -s "$setup_pkg_file" ] || return 0
+	setup_unknown=$setup_work/unknown-packages
+	awk -F '\t' '
+		FNR == NR {
+			gsub(/\r/, "")
+			if ($1 != "")
+				want[$1] = 1
+			next
+		}
+		{ have[$1] = 1 }
+		END {
+			for (n in want)
+				if (!(n in have))
+					print n
+		}
+	' "$setup_pkg_file" "$setup_index" | LC_ALL=C sort >"$setup_unknown"
+	[ -s "$setup_unknown" ] || return 0
+	printf '%s: packages not in the recipe index:\n' "$sps_prog" >&2
+	sed 's/^/  /' "$setup_unknown" >&2
+	setup_fail "$SPS_EX_NOTFOUND" \
+		"requested packages are not packaged (see the list above)"
+}
+
 setup_work=
 setup_interactive=1
 setup_plan=0
@@ -817,6 +866,7 @@ fi
 
 setup_work=$(sps_mktemp_dir "${TMPDIR:-/tmp}" setup) || exit $?
 trap setup_cleanup 0 HUP INT TERM
+setup_collect_recipe_names "$setup_work/recipe-names"
 
 if [ "$setup_interactive" -eq 1 ]; then
 	if [ -f /etc/sps/live ]; then
@@ -944,6 +994,15 @@ Use /mnt after mounting the destination filesystem, or after a guided layout (se
 			setup_et=${setup_exline#*	}
 			[ -n "$setup_en" ] || continue
 			[ "$setup_et" != "$setup_exline" ] || setup_et=$setup_en
+			# Live recipe trees are the source of truth. Unpackaged
+			# names (mpv, LibreOffice, ...) must not reach sget.
+			if [ -s "$setup_work/recipe-names" ] &&
+				! grep -qx "$setup_en" "$setup_work/recipe-names"
+			then
+				printf '%s: skipping unpackaged extra %s\n' \
+					"$sps_prog" "$setup_en" >&2
+				continue
+			fi
 			setup_on=off
 			case " $setup_extra " in *" $setup_en "*) setup_on=on ;; esac
 			setup_ex_args="$setup_ex_args $setup_en '$setup_et' $setup_on"
@@ -1706,6 +1765,8 @@ if [ "$setup_do_update" -eq 1 ]; then
 	src update || setup_fail "$SPS_EX_NETWORK" "src update failed"
 fi
 
+setup_reject_unknown_packages
+
 setup_pkgs=
 while IFS= read -r setup_pkg; do
 	[ -n "$setup_pkg" ] || continue
diff --git a/lib/deps.awk b/lib/deps.awk
index 88bd024..097ccbf 100644
--- a/lib/deps.awk
+++ b/lib/deps.awk
@@ -14,8 +14,7 @@ function deps_stderr(message,    command) {
 }
 
 function deps_error(message) {
-    if (!deps_failed)
-        deps_stderr((program == "" ? "sget" : program) ": " message)
+    deps_stderr((program == "" ? "sget" : program) ": " message)
     deps_failed = 1
 }
 
@@ -96,8 +95,6 @@ function deps_cycle(package,    first, i, message) {
 }
 
 function deps_visit(package, parent,    list, count, items, i, child) {
-    if (deps_failed)
-        return
     if (!(package in deps_selected_priority)) {
         if (parent == "")
             deps_error("package '" package "' not found")
@@ -123,10 +120,8 @@ function deps_visit(package, parent,    list, count, items, i, child) {
         for (i = 1; i <= count; i++) {
             child = deps_dependency_name(items[i], package)
             if (child == "")
-                break
+                continue
             deps_visit(child, package)
-            if (deps_failed)
-                break
         }
     }
 
@@ -210,11 +205,8 @@ END {
         exit 5
     if (action == "validate") {
         deps_sort_names(deps_names, deps_name_count)
-        for (deps_i = 1; deps_i <= deps_name_count; deps_i++) {
+        for (deps_i = 1; deps_i <= deps_name_count; deps_i++)
             deps_visit(deps_names[deps_i], "")
-            if (deps_failed)
-                break
-        }
         if (deps_failed)
             exit 5
         exit 0
@@ -229,11 +221,9 @@ END {
         if (deps_roots[deps_i] !~ \
             /^[A-Za-z0-9][A-Za-z0-9+_.-]*$/) {
             deps_error("invalid package name '" deps_roots[deps_i] "'")
-            break
+            continue
         }
         deps_visit(deps_roots[deps_i], "")
-        if (deps_failed)
-            break
     }
     if (deps_failed)
         exit 5
diff --git a/lib/mkiso/live-plasma b/lib/mkiso/live-plasma
index 0cd960c..17112e3 100644
--- a/lib/mkiso/live-plasma
+++ b/lib/mkiso/live-plasma
@@ -10,7 +10,9 @@ export 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
+export XDG_CONFIG_DIRS=/etc/xdg
 export TERM=${TERM:-linux}
 export LANG=${LANG:-C.UTF-8}
 export LC_ALL=${LC_ALL:-C.UTF-8}
@@ -38,6 +40,22 @@ if [ ! -s /root/.config/kdedefaults/ksplashrc ]; then
 	printf '%s\n' '[KSplash]' 'Engine=KSplashQML' \
 		'Theme=org.kde.breeze.desktop' >/root/.config/kdedefaults/ksplashrc
 fi
+# Live runs as root with no PAM stack. kscreenlocker_greet missing or
+# autolocking leaves KWin's lock surface up: black screen, cursor only.
+mkdir -p /etc/xdg /root/.config
+if [ ! -s /etc/xdg/kscreenlockerrc ]; then
+	printf '%s\n' \
+		'[Daemon]' \
+		'Autolock=false' \
+		'LockOnResume=false' \
+		'Timeout=0' >/etc/xdg/kscreenlockerrc
+fi
+if [ ! -s /root/.config/kscreenlockerrc ]; then
+	cp /etc/xdg/kscreenlockerrc /root/.config/kscreenlockerrc
+fi
+if command -v fc-cache >/dev/null 2>&1; then
+	fc-cache -f >/dev/null 2>&1 || :
+fi
 
 # Live NVIDIA is nouveau. Keep the proprietary stack unloaded.
 modprobe nouveau 2>/dev/null || :
@@ -131,7 +149,8 @@ if command -v startplasma-wayland >/dev/null 2>&1; then
 	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
+		exec dbus-run-session -- startplasma-wayland \
+			>>/run/plasma.log 2>&1
 	fi
 	if command -v dbus-launch >/dev/null 2>&1; then
 		eval "$(dbus-launch --sh-syntax)"
diff --git a/lib/mkiso/plasma-bins b/lib/mkiso/plasma-bins
index 7f301c5..a5f09fe 100644
--- a/lib/mkiso/plasma-bins
+++ b/lib/mkiso/plasma-bins
@@ -51,6 +51,8 @@
 /usr/libexec/kf6/kio_http_cache_cleaner
 /usr/libexec/kf6/kiod6
 /usr/libexec/kf6/kscreen_backend_launcher
+/usr/libexec/kactivitymanagerd
+/usr/libexec/kscreenlocker_greet
 /usr/libexec/plasma-dbus-run-session-if-needed
 /usr/libexec/plasma-sourceenv.sh
 /usr/libexec/plasma-startup-sound
diff --git a/lib/mkiso/plasma-trees b/lib/mkiso/plasma-trees
index 951bd6d..1cf4187 100644
--- a/lib/mkiso/plasma-trees
+++ b/lib/mkiso/plasma-trees
@@ -36,6 +36,8 @@
 /usr/lib64/gbm
 /usr/share/kwin
 /usr/share/kwin-wayland
+/usr/share/kf6
+/usr/share/wallpapers/Next
 /etc/fonts
 /usr/share/fontconfig
 /usr/share/glvnd
diff --git a/lib/setup/extras b/lib/setup/extras
index 1b14166..19a1062 100644
--- a/lib/setup/extras
+++ b/lib/setup/extras
@@ -1,90 +1,82 @@
-neovim	Neovim
-vim	Vim
-nano	GNU nano
-emacs	GNU Emacs
-helix	Helix editor
-kakoune	Kakoune editor
-htop	htop
+# Extra packages offered by setup(1). One name<TAB>title per line.
+# Names must exist as recipes in sps-core or sps-extra, appear once,
+# and stay in LC_ALL=C sort order. tests/test_setup_catalog.sh and
+# mkiso refuse anything else. Do not list unpackaged names (mpv, GIMP,
+# LibreOffice, and similar); the extra set or a later recipe is the place
+# for those.
+alsa-utils	ALSA utilities
+amd-ucode	AMD CPU microcode
+bat	bat
+bluez	BlueZ Bluetooth
 btop	btop
-tmux	tmux
-sudo	sudo
+cmake	CMake
+cryptsetup	cryptsetup
+cups	CUPS printing
+curl	curl
+dhcpcd	dhcpcd
 doas	OpenDoas
-zsh	Z shell
+dosfstools	dosfstools
+e2fsprogs	e2fsprogs
+emacs	GNU Emacs
+ethtool	ethtool
+exfatprogs	exFAT tools
+eza	eza
+falkon	Falkon
+fastfetch	fastfetch
+fd	fd
+firefox	Firefox
 fish	fish shell
+flatpak	Flatpak
+fzf	fzf
+gdb	GDB
 git	Git
-rsync	rsync
-openssh	OpenSSH
 gnupg	GnuPG
-firefox	Firefox
-falkon	Falkon
-flatpak	Flatpak
-pipewire	PipeWire
-wget	wget
-curl	curl
+go	Go
+gptfdisk	GPT fdisk
+helix	Helix editor
+htop	htop
+intel-ucode	Intel CPU microcode
+iperf3	iperf3
+iwd	iwd
 jq	jq
-ripgrep	ripgrep
-fd	fd
-bat	bat
-eza	eza
-fzf	fzf
+kakoune	Kakoune editor
 lazygit	lazygit
-fastfetch	fastfetch
-python	Python
+linux	Linux kernel, upstream defconfig
+linux-desktop	Linux kernel, daily desktop config
+lsof	lsof
 lua	Lua
-go	Go
-rust	Rust
-nodejs	Node.js
-perl	Perl
-ruby	Ruby
-cmake	CMake
+mdadm	Linux MD RAID
+mesa	Mesa
 meson	Meson
+micro	micro editor
+nano	GNU nano
+neovim	Neovim
+networkmanager	NetworkManager
+nftables	nftables
 ninja	Ninja
-gdb	GDB
-strace	strace
-lsof	lsof
 nmap	nmap
-iperf3	iperf3
-cups	CUPS printing
-sddm	SDDM
-mesa	Mesa
+nodejs	Node.js
+nouveau	Nouveau (NVIDIA open driver)
+ntfs-3g	NTFS driver
 nvidia-open	NVIDIA open kernel modules
-e2fsprogs	e2fsprogs
-dosfstools	dosfstools
-cryptsetup	cryptsetup
-iwd	iwd
-dhcpcd	dhcpcd
-micro	micro editor
-starship	starship prompt
-yq	yq
-networkmanager	NetworkManager
+openssh	OpenSSH
 parted	GNU parted
-gptfdisk	GPT fdisk
-ntfs-3g	NTFS driver
-exfatprogs	exFAT tools
-mdadm	Linux MD RAID
+perl	Perl
+pipewire	PipeWire
+python	Python
+ripgrep	ripgrep
+rsync	rsync
+ruby	Ruby
+rust	Rust
+sddm	SDDM
 smartmontools	S.M.A.R.T. tools
-bluez	BlueZ Bluetooth
-alsa-utils	ALSA utilities
+starship	starship prompt
+strace	strace
+sudo	sudo
+tmux	tmux
+vim	Vim
 vulkan-tools	vulkaninfo
-ethtool	ethtool
-nftables	nftables
+wget	wget
 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
-
-
+yq	yq
+zsh	Z shell
diff --git a/man/mkpkg.1 b/man/mkpkg.1
index da8ea54..1109973 100644
--- a/man/mkpkg.1
+++ b/man/mkpkg.1
@@ -153,6 +153,23 @@ provides per-package parallelism and is exposed to build commands as
 contains downloaded sources;
 .B SPS_BUILD
 contains temporary build/staging trees.
+When
+.B SPS_ROOT
+is not
+.BR / ,
+every build phase also searches that root for headers, libraries, and
+pkg-config files
+.RB ( CFLAGS ,
+.BR LDFLAGS ,
+.BR LIBRARY_PATH ,
+.BR PKG_CONFIG_SYSROOT_DIR ,
+.BR CMAKE_PREFIX_PATH ).
+Live
+.BR setup (1)
+installs into a mounted target with the live compiler; without this, a
+package cannot link against a library
+.BR pkin (1)
+just unpacked into the target.
 With
 .BR --build-root ,
 .B SYSROOT
diff --git a/man/setup.1 b/man/setup.1
index 98232da..b4a0f78 100644
--- a/man/setup.1
+++ b/man/setup.1
@@ -147,7 +147,9 @@ Choosing zsh or fish also enables the
 set.
 .TP
 .BI --extra " pkg[,pkg...]"
-Individual packages in addition to the profile and sets.
+Individual packages in addition to the profile and sets. The extras
+screen only lists names that exist as recipes in sps-core or sps-extra.
+Unpackaged names are refused after the repository index is built.
 .TP
 .B --ssh yes|no
 Enable the OpenSSH set.
diff --git a/man/sget.1 b/man/sget.1
index 6738115..7faea8a 100644
--- a/man/sget.1
+++ b/man/sget.1
@@ -31,6 +31,15 @@ with the terminal attached so compiler and build-system output appear live.
 reports the finished archive;
 .BR pkin (1)
 reports installation. An exact cached archive is reused without a build.
+When
+.B SPS_ROOT
+is not
+.BR / ,
+.B mkpkg
+compiles and links against libraries already installed in that root so
+each package can see the previous one. Live
+.BR setup (1)
+depends on this.
 .SH COMMANDS
 .TP
 .BI search " pattern"
diff --git a/tests/test_build.sh b/tests/test_build.sh
index 6afa655..f0c10e3 100755
--- a/tests/test_build.sh
+++ b/tests/test_build.sh
@@ -198,4 +198,52 @@ esac
 [ ! -e "$tmp/root/usr/share/sysroot-probe" ] ||
 	fail "builder modified the target root with a build root"
 
+# When SPS_ROOT is a target disk, gcc must find libraries pkin already
+# installed there. Copying host libasound onto the live ISO would only
+# paper over alsa-utils; the next package would fail the same way.
+if command -v gcc >/dev/null 2>&1; then
+	mkdir -p "$tmp/root/usr/include" "$tmp/root/usr/lib64" \
+		"$tmp/link-recipe" "$tmp/link-out"
+	cat >"$tmp/root/usr/include/spsprobe.h" <<'EOF'
+int sps_probe_lib(void);
+EOF
+	cat >"$tmp/spsprobe.c" <<'EOF'
+int sps_probe_lib(void) { return 0; }
+EOF
+	gcc -shared -fPIC -o "$tmp/root/usr/lib64/libspsprobe.so" \
+		"$tmp/spsprobe.c" ||
+		fail "could not build the target-root probe library"
+	cat >"$tmp/link-recipe/recipe" <<'EOF'
+name        target-sysroot-probe
+version     1.0
+release     1
+arch        any
+description Confirms SPS_ROOT is searched for headers and libraries
+configure   printf '%s\n' "${PKG_CONFIG_SYSROOT_DIR-}" >"$WORK/sysroot"
+configure   printf '%s\n' "$LIBRARY_PATH" >"$WORK/libpath"
+configure   printf '%s\n' '#include <spsprobe.h>' 'int main(void){return sps_probe_lib();}' >"$WORK/probe.c"
+configure   gcc "$WORK/probe.c" -lspsprobe -o "$WORK/probe"
+install     mkdir -p "$PKG/usr/share/target-sysroot-probe" "$PKG/usr/bin"
+install     cp "$WORK/sysroot" "$WORK/libpath" "$PKG/usr/share/target-sysroot-probe/"
+install     cp "$WORK/probe" "$PKG/usr/bin/target-sysroot-probe"
+install     chmod 755 "$PKG/usr/bin/target-sysroot-probe"
+EOF
+	link_artifact=$(run_mkpkg --no-download --output "$tmp/link-out" \
+		"$tmp/link-recipe/recipe") ||
+		fail "target-root library probe failed to build"
+	mkdir "$tmp/link-extract"
+	tar -xf "$link_artifact" -C "$tmp/link-extract"
+	assert_equal "$tmp/root" \
+		"$(cat "$tmp/link-extract/usr/share/target-sysroot-probe/sysroot")" \
+		"PKG_CONFIG_SYSROOT_DIR was not set to SPS_ROOT"
+	case "$(cat "$tmp/link-extract/usr/share/target-sysroot-probe/libpath")" in
+		"$tmp/root/usr/lib64:"*) : ;;
+		*) fail "LIBRARY_PATH did not start with the target libdir" ;;
+	esac
+	[ -x "$tmp/link-extract/usr/bin/target-sysroot-probe" ] ||
+		fail "probe binary was not staged"
+	# The .so lives only under SPS_ROOT, so a successful link means gcc
+	# searched the target rather than the live /usr/lib64.
+fi
+
 printf '%s\n' 'test_build: ok'
\ No newline at end of file
diff --git a/tests/test_dependencies.sh b/tests/test_dependencies.sh
index cb724f0..1cbe8f3 100755
--- a/tests/test_dependencies.sh
+++ b/tests/test_dependencies.sh
@@ -273,4 +273,12 @@ expect_stale_definition hook
 printf '%s\n' '# changed after indexing' >> "$tmp/repo/app/recipe"
 expect_stale_definition recipe
 
+set +e
+"$sget" install nosuch-a nosuch-b >"$tmp/miss.out" 2>"$tmp/miss.err"
+miss_st=$?
+set -e
+[ "$miss_st" -eq 5 ] || fail "two missing packages returned $miss_st"
+contains "$(cat "$tmp/miss.err")" "package 'nosuch-a' not found"
+contains "$(cat "$tmp/miss.err")" "package 'nosuch-b' not found"
+
 printf '%s\n' 'dependency resolution and install orchestration tests passed'
diff --git a/tests/test_mkiso.sh b/tests/test_mkiso.sh
index 9ad986b..3a68680 100755
--- a/tests/test_mkiso.sh
+++ b/tests/test_mkiso.sh
@@ -226,3 +226,35 @@ then
 		fi
 	done
 fi
+grep -q 'copying file(1) magic database' "$mkiso" ||
+	fail 'seeded ISO must copy file(1) magic for autoconf'
+grep -q '/etc/file' "$mkiso" ||
+	fail 'mkiso must copy /etc/file magic used by this host file(1)'
+grep -q kactivitymanagerd "$project_dir/lib/mkiso/plasma-bins" ||
+	fail 'plasma-bins must include kactivitymanagerd'
+grep -q kscreenlocker_greet "$project_dir/lib/mkiso/plasma-bins" ||
+	fail 'plasma-bins must include kscreenlocker_greet'
+grep -q kscreenlockerrc "$mkiso" ||
+	fail 'mkiso must bake kscreenlockerrc so live Plasma does not autolock'
+grep -q Liberation "$mkiso" ||
+	fail 'plasma ISO must copy Liberation fonts matching host fontconfig'
+grep -q XDG_CURRENT_DESKTOP "$project_dir/lib/mkiso/live-plasma" ||
+	fail 'live-plasma must set XDG_CURRENT_DESKTOP=KDE'
+grep -q mkiso_copy_plasma_runtime_deps "$mkiso" ||
+	fail 'plasma ISO must ldd session helpers (kactivitymanagerd, greeter)'
+grep -q 'using .* for headers, libraries, and pkg-config' "$project_dir/bin/mkpkg" ||
+	fail 'mkpkg must compile against SPS_ROOT so setup can link target libs'
+grep -q mkiso_verify_setup_catalog "$mkiso" ||
+	fail 'mkiso must refuse extras/sets that name unpackaged recipes'
+if grep -E '^(htop|btop|tmux)[[:space:]]' "$project_dir/lib/setup/extras" |
+	awk '{c[$1]++} END { for (n in c) if (c[n] > 1) exit 1 }'
+then
+	:
+else
+	fail 'extras must not list htop, btop, or tmux more than once'
+fi
+if grep -E '^(mpv|gimp|libreoffice|qemu|screen)[[:space:]]' \
+	"$project_dir/lib/setup/extras"
+then
+	fail 'extras must not offer unpackaged names such as mpv'
+fi
diff --git a/tests/test_setup_catalog.sh b/tests/test_setup_catalog.sh
new file mode 100644
index 0000000..162cead
--- /dev/null
+++ b/tests/test_setup_catalog.sh
@@ -0,0 +1,120 @@
+#!/bin/sh
+# setup extras/sets/profiles must name real, unique, sorted packages.
+
+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_setup_catalog: %s\n' "$*" >&2; exit 1; }
+
+setup_dir=$project_dir/lib/setup
+extras=$setup_dir/extras
+[ -f "$extras" ] || fail 'lib/setup/extras is missing'
+
+# Unique, tab-separated, LC_ALL=C sorted. Duplicate htop/btop rows and
+# unpackaged names such as mpv are how the extras checklist lied to setup.
+LC_ALL=C awk '
+	BEGIN { bad = 0; count = 0 }
+	/^[[:space:]]*$/ || /^#/ { next }
+	{
+		tab = index($0, "\t")
+		if (tab == 0) {
+			print "extras line has no tab: " $0
+			bad = 1
+			next
+		}
+		name = substr($0, 1, tab - 1)
+		if (name !~ /^[A-Za-z0-9][A-Za-z0-9+_.-]*$/) {
+			print "extras bad name: " name
+			bad = 1
+		}
+		if (name in seen) {
+			print "extras duplicate: " name
+			bad = 1
+		}
+		seen[name] = 1
+		count++
+		names[count] = name
+	}
+	END {
+		for (i = 1; i < count; i++)
+			if (names[i] > names[i + 1]) {
+				print "extras not sorted: " names[i] " before " names[i + 1]
+				bad = 1
+			}
+		if (count < 2) {
+			print "extras is empty"
+			bad = 1
+		}
+		exit bad
+	}
+' "$extras" || fail 'extras must be unique, tab-separated, and sorted'
+
+tmp=$(mktemp -d "${TMPDIR:-/tmp}/sps-test-catalog.XXXXXX") || fail mktemp
+trap 'rm -rf "$tmp"' 0 HUP INT TERM
+
+awk -F '\t' '/^[[:space:]]*$/ || /^#/ { next } { print $1 }' "$extras" \
+	>"$tmp/wanted"
+for f in "$setup_dir"/sets/*.set "$setup_dir"/profiles/*.profile
+do
+	[ -f "$f" ] || continue
+	awk '/^package[[:space:]]/ { print $2 }' "$f" >>"$tmp/wanted"
+done
+	if [ -f "$setup_dir/hwdetect.sh" ]; then
+		awk '
+			/printf '\''%s\\n'\'' [a-z]/ && $0 !~ /\$/ && $0 !~ /\|/ {
+				for (i = 3; i <= NF; i++) {
+					gsub(/;/, "", $i)
+					if ($i ~ /^[a-z][a-z0-9+-]*$/)
+						print $i
+				}
+			}
+		' "$setup_dir/hwdetect.sh" >>"$tmp/wanted"
+	fi
+LC_ALL=C sort -u "$tmp/wanted" -o "$tmp/wanted"
+
+# Recipe trees are optional for a tools-only checkout. When they are
+# present, every setup name must exist so live setup cannot offer mpv.
+catalog_trees=
+for d in \
+	${SPS_CORE:-} ${SPS_EXTRA:-} \
+	${ISO_CORE:-} ${ISO_EXTRA:-} \
+	/tmp/sps-core /tmp/sps-extra \
+	/usr/src/sps/core /usr/src/sps/extra
+do
+	[ -n "$d" ] || continue
+	[ -d "$d" ] || continue
+	case " $catalog_trees " in *" $d "*) continue ;; esac
+	catalog_trees="$catalog_trees $d"
+done
+
+if [ -z "$catalog_trees" ]; then
+	printf '%s\n' \
+		'test_setup_catalog: no recipe trees; skipped existence check'
+	printf '%s\n' 'test_setup_catalog: ok'
+	exit 0
+fi
+
+: >"$tmp/recipes"
+for tree in $catalog_trees
+do
+	find "$tree" \( -type d \( -name .git -o -name files -o \
+		-name patches -o -name hooks \) -prune \) -o \
+		-type f -name recipe -print
+done | while IFS= read -r recipe || [ -n "$recipe" ]
+do
+	[ -n "$recipe" ] || continue
+	awk '/^name[[:space:]]/ { print $2; exit }' "$recipe"
+done | LC_ALL=C sort -u >"$tmp/recipes"
+[ -s "$tmp/recipes" ] || fail 'recipe trees contained no name records'
+
+miss=
+while IFS= read -r pkg || [ -n "$pkg" ]
+do
+	[ -n "$pkg" ] || continue
+	grep -qx "$pkg" "$tmp/recipes" && continue
+	miss="$miss $pkg"
+done <"$tmp/wanted"
+[ -z "$miss" ] || fail "setup names packages with no recipe:$miss"
+
+printf '%s\n' 'test_setup_catalog: ok'