Splux Linux

source-based Linux using SPS

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

bin/mkiso


Shell

73283 bytes at 2909919 · GitHub

#!/bin/sh
# 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
# stdlib, Meson (mesonbuild), Perl library. Host /usr/bin/meson is a
# setuptools wrapper; mesonbuild lives under site-packages, not next
# to the Python stdlib, so copying python3.12 is not enough.
# 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
if [ -z "${SPS_LIBDIR+x}" ]; then
	if [ -r "$sps_bin_dir/../lib/common.sh" ]; then
		SPS_LIBDIR=$sps_bin_dir/../lib
	else
		SPS_LIBDIR=$sps_bin_dir/../lib/sps
	fi
fi
if ! . "$SPS_LIBDIR/common.sh"; then
	printf '%s\n' "mkiso: cannot load $SPS_LIBDIR/common.sh" >&2
	exit 1
fi

mkiso_usage()
{
	cat <<'EOF'
usage: mkiso --output FILE.iso [options]
       mkiso --layout DIR [options]

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)
  --busybox FILE        static busybox for the initramfs
  --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 (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
}

mkiso_output=
mkiso_layout=
mkiso_work=
mkiso_work_created=0
mkiso_kernel=
mkiso_modules=
mkiso_busybox=
mkiso_core=
mkiso_extra=
mkiso_with_rust=0
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()
{
	[ "$#" -ge 2 ] && [ -n "${2-}" ] ||
		sps_die "$SPS_EX_USAGE" "option '$1' requires an argument"
}

while [ "$#" -gt 0 ]; do
	case $1 in
		-h|--help)
			mkiso_usage
			exit 0
			;;
		--output)
			mkiso_need_value "$@"
			mkiso_output=$2
			shift 2
			;;
		--layout)
			mkiso_need_value "$@"
			mkiso_layout=$2
			shift 2
			;;
		--work)
			mkiso_need_value "$@"
			mkiso_work=$2
			shift 2
			;;
		--kernel)
			mkiso_need_value "$@"
			mkiso_kernel=$2
			shift 2
			;;
		--modules)
			mkiso_need_value "$@"
			mkiso_modules=$2
			shift 2
			;;
		--busybox)
			mkiso_need_value "$@"
			mkiso_busybox=$2
			shift 2
			;;
		--core)
			mkiso_need_value "$@"
			mkiso_core=$2
			shift 2
			;;
		--extra)
			mkiso_need_value "$@"
			mkiso_extra=$2
			shift 2
			;;
		--with-rust) mkiso_with_rust=1; shift ;;
		--with-go) mkiso_with_go=1; shift ;;
		--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"
			;;
		*)
			sps_die "$SPS_EX_USAGE" "unexpected argument: $1"
			;;
	esac
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|s6|runit|dinit|shepherd|sysvinit) ;;
	*) sps_die "$SPS_EX_USAGE" "init must be systemd, openrc, s6, runit, dinit, shepherd, or sysvinit" ;;
esac

mkiso_cleanup()
{
	if [ "$mkiso_work_created" -eq 1 ] && [ -n "$mkiso_work" ] &&
	   [ -d "$mkiso_work" ]; then
		rm -rf "$mkiso_work"
	fi
}

if [ -z "$mkiso_work" ]; then
	mkiso_work=$(sps_mktemp_dir "${TMPDIR:-/tmp}" mkiso) || exit $?
	mkiso_work_created=1
	trap mkiso_cleanup 0 HUP INT TERM
else
	case $mkiso_work in
		/*) ;;
		*) sps_die "$SPS_EX_USAGE" "work directory must be absolute" ;;
	esac
	mkdir -p "$mkiso_work" ||
		sps_die "$SPS_EX_PERMISSION" "cannot create $mkiso_work"
fi

if [ -n "$mkiso_layout" ]; then
	case $mkiso_layout in
		/*) ;;
		*) sps_die "$SPS_EX_USAGE" "layout directory must be absolute" ;;
	esac
	mkiso_live=$mkiso_layout
else
	mkiso_live=$mkiso_work/live-root
fi

mkdir -p "$mkiso_live" ||
	sps_die "$SPS_EX_PERMISSION" "cannot create live root"

# --- copy helpers -------------------------------------------------------

# Copy a host path into the live root. Directories are merged (src/. into
# dest/) so a second copy of /usr/lib64/qt6 cannot nest as qt6/qt6.
# Symlinks are copied as links, then the chain is followed so sonames like
# libtinfo.so.6 are not left dangling.
mkiso_copy_path()
{
	mkiso_src=$1
	mkiso_depth=${2:-0}
	[ "$mkiso_depth" -lt 16 ] || return 0
	[ -e "$mkiso_src" ] || [ -L "$mkiso_src" ] || return 0
	mkiso_dest=$mkiso_live$mkiso_src
	mkdir -p "$(dirname "$mkiso_dest")" || return 0
	if [ -d "$mkiso_src" ] && [ ! -L "$mkiso_src" ]; then
		mkdir -p "$mkiso_dest"
		cp -a --reflink=auto "$mkiso_src"/. "$mkiso_dest"/ 2>/dev/null ||
			cp -a "$mkiso_src"/. "$mkiso_dest"/ 2>/dev/null ||
			cp -R "$mkiso_src"/. "$mkiso_dest"/ 2>/dev/null || :
	else
		# usr-merge puts busybox applets in usr/bin. cp onto a
		# symlink follows it and overwrites busybox itself.
		if [ -L "$mkiso_dest" ]; then
			rm -f "$mkiso_dest"
		fi
		cp -a --reflink=auto "$mkiso_src" "$mkiso_dest" 2>/dev/null ||
			cp -a "$mkiso_src" "$mkiso_dest" 2>/dev/null ||
			cp "$mkiso_src" "$mkiso_dest" 2>/dev/null || :
		if [ -L "$mkiso_src" ]; then
			mkiso_link=$(readlink "$mkiso_src") || return 0
			case $mkiso_link in
				/*) mkiso_next=$mkiso_link ;;
				*) mkiso_next=$(dirname "$mkiso_src")/$mkiso_link ;;
			esac
			mkiso_copy_path "$mkiso_next" $((mkiso_depth + 1))
		fi
	fi
}

mkiso_copy_libs()
{
	mkiso_bin=$1
	[ -f "$mkiso_bin" ] || [ -L "$mkiso_bin" ] || return 0
	command -v ldd >/dev/null 2>&1 || return 0
	ldd "$mkiso_bin" 2>/dev/null | while IFS= read -r mkiso_line; do
		mkiso_lib=
		case $mkiso_line in
			*linux-vdso*|*'not a dynamic'*) continue ;;
		esac
		# "libx.so => /lib64/libx.so (0x...)"
		mkiso_lib=${mkiso_line#*=> }
		mkiso_lib=${mkiso_lib%% (*}
		mkiso_lib=${mkiso_lib## }
		case $mkiso_lib in
			/*) mkiso_copy_path "$mkiso_lib" ;;
		esac
		# interpreter-only lines: "/lib64/ld-linux-x86-64.so.2 (0x...)"
		case $mkiso_line in
			"	/"*|" /"*)
				mkiso_interp=${mkiso_line%% (*}
				mkiso_interp=${mkiso_interp## }
				mkiso_interp=${mkiso_interp#	}
				case $mkiso_interp in
					/*) mkiso_copy_path "$mkiso_interp" ;;
				esac
				;;
		esac
		# An ldd line reads "WANTED => RESOLVED". For a library, WANTED is
		# a soname the loader searches for, and copying RESOLVED is right.
		# For the program interpreter, WANTED is the absolute path baked
		# into the ELF, and the kernel opens exactly that. When the two
		# differ because the host reaches the loader through a symlinked
		# lib64, copying only RESOLVED leaves the interpreter missing at
		# the one path that matters, and exec fails with ENOENT. The
		# kernel then reports it as "can't execute /sbin/init".
		case $mkiso_line in
			*" => "*)
				mkiso_wanted=${mkiso_line%% => *}
				mkiso_wanted=${mkiso_wanted## }
				mkiso_wanted=${mkiso_wanted#	}
				case $mkiso_wanted in
					/*)
						[ "$mkiso_wanted" = "$mkiso_lib" ] ||
							mkiso_copy_as "$mkiso_lib" "$mkiso_wanted"
						;;
				esac
				;;
		esac
	done
}

# Place the contents of an existing host file at a specific path in the live
# root, which may differ from where it lives on the host.
mkiso_copy_as()
{
	mkiso_src=$1
	mkiso_dst=$2
	[ -n "$mkiso_src" ] && [ -e "$mkiso_src" ] || return 0
	[ -e "$mkiso_live$mkiso_dst" ] && return 0
	mkdir -p "$mkiso_live${mkiso_dst%/*}" || return 0
	cp -a "$mkiso_src" "$mkiso_live$mkiso_dst" 2>/dev/null || return 0
}

# close_libs only walks bin/sbin. cc1 (libmpc/libisl) and Mesa DRI
# (libSPIRV-Tools) are dlopened helpers, so a seeded disc can have gcc
# and still fail "C compiler cannot create executables", and Plasma can
# have /dev/dri/card0 and still report "No suitable DRM devices".
mkiso_copy_so_deps()
{
	mkiso_so=$1
	[ -e "$mkiso_so" ] || [ -L "$mkiso_so" ] || return 0
	case $mkiso_so in
		*.a|*-rust*|*nvidia*) return 0 ;;
	esac
	mkiso_copy_path "$mkiso_so"
	mkiso_copy_libs "$mkiso_so"
}

mkiso_copy_gcc_deps()
{
	command -v find >/dev/null 2>&1 || return 0
	printf '%s: resolving gcc cc1 libraries\n' "$sps_prog" >&2
	for mkiso_d in /usr/libexec/gcc /usr/lib64/gcc /usr/lib/gcc
	do
		[ -d "$mkiso_d" ] || continue
		find "$mkiso_d" -type f \( \
			-name cc1 -o -name cc1plus -o -name collect2 -o \
			-name lto-wrapper -o -name lto1 -o \
			-name liblto_plugin.so -o -name 'liblto_plugin.so.*' \
			\) -print 2>/dev/null |
		while IFS= read -r mkiso_elf || [ -n "$mkiso_elf" ]; do
			[ -n "$mkiso_elf" ] || continue
			mkiso_copy_so_deps "$mkiso_elf"
		done
	done
}

# Python's stdlib is under lib64; Meson is a pure-Python package under
# /usr/lib/pythonX.Y/site-packages. Copying the interpreter and
# lib64/pythonX.Y is not enough: that tree's site-packages is PyQt and
# friends, not mesonbuild. The setuptools /usr/bin/meson then dies with
# ModuleNotFoundError, which looks like a Python error at the first
# meson recipe (often libxml2).
# The live root's interpreter is the /usr/bin/python3 that seed-bins
# copies, so seed the stdlib and Meson for that same interpreter. A PATH
# that puts a relocated python (Linuxbrew, pyenv) first must not drag the
# seed onto a different minor version.
mkiso_seed_python()
{
	for mkiso_probe in /usr/bin/python3 /usr/bin/python3.12 python3
	do
		mkiso_probe_cmd=$(command -v "$mkiso_probe" 2>/dev/null) || continue
		[ -x "$mkiso_probe_cmd" ] || continue
		printf '%s\n' "$mkiso_probe_cmd"
		return 0
	done
	return 1
}

mkiso_python_version()
{
	mkiso_py_probe=$(mkiso_seed_python) ||
		mkiso_py_probe=/usr/bin/python3
	"$mkiso_py_probe" -c \
		'import sys; print("%d.%d" % sys.version_info[:2])' \
		2>/dev/null || printf '%s\n' 3.12
}

mkiso_copy_python_stdlib()
{
	mkiso_pyver=$(mkiso_python_version)
	mkiso_seeded_pylib=
	for mkiso_pylib in /usr/lib64/python$mkiso_pyver \
		/usr/lib/python$mkiso_pyver
	do
		[ -d "$mkiso_pylib" ] || continue
		if [ -d "$mkiso_pylib/encodings" ]; then
			mkiso_seeded_pylib=$mkiso_pylib
		fi
		printf '%s: copying Python %s stdlib from %s\n' \
			"$sps_prog" "$mkiso_pyver" "$mkiso_pylib" >&2
		for mkiso_pyent in "$mkiso_pylib"/*; do
			[ -e "$mkiso_pyent" ] || continue
			mkiso_pybase=${mkiso_pyent##*/}
			case $mkiso_pybase in
				test|idlelib|ensurepip|turtledemo|site-packages)
					continue
					;;
			esac
			mkiso_copy_path "$mkiso_pyent"
		done
		if [ -d "$mkiso_pylib/lib-dynload" ]; then
			for mkiso_pyso in "$mkiso_pylib/lib-dynload"/*.so \
				"$mkiso_pylib/lib-dynload"/*.so.*
			do
				[ -e "$mkiso_pyso" ] || continue
				mkiso_copy_libs "$mkiso_pyso"
			done
		fi
	done
	# Relocated pythons (Linuxbrew, pyenv) keep stdlib beside the
	# interpreter, not under /usr/lib. Map it onto the standard live
	# path so the seeded interpreter finds encodings and modules.
	if [ -z "$mkiso_seeded_pylib" ]; then
		mkiso_py_probe=$(mkiso_seed_python) ||
			mkiso_py_probe=/usr/bin/python3
		mkiso_pysrc=$("$mkiso_py_probe" -c \
			'import sys, os; print(os.path.join(sys.base_prefix, "lib", "python%d.%d" % sys.version_info[:2]))' \
			2>/dev/null) || mkiso_pysrc=
		if [ -n "$mkiso_pysrc" ] && [ -d "$mkiso_pysrc" ] &&
			[ -d "$mkiso_pysrc/encodings" ]
		then
			printf '%s: copying Python %s stdlib from %s\n' \
				"$sps_prog" "$mkiso_pyver" "$mkiso_pysrc" >&2
			mkdir -p "$mkiso_live/usr/lib/python$mkiso_pyver"
			for mkiso_pyent in "$mkiso_pysrc"/*; do
				[ -e "$mkiso_pyent" ] || continue
				mkiso_pybase=${mkiso_pyent##*/}
				case $mkiso_pybase in
					test|idlelib|ensurepip|turtledemo|site-packages)
						continue
						;;
				esac
				cp -a --reflink=auto "$mkiso_pyent" \
					"$mkiso_live/usr/lib/python$mkiso_pyver/" \
					2>/dev/null ||
					cp -a "$mkiso_pyent" \
						"$mkiso_live/usr/lib/python$mkiso_pyver/" \
					2>/dev/null ||
					cp -R "$mkiso_pyent" \
						"$mkiso_live/usr/lib/python$mkiso_pyver/" \
					2>/dev/null || :
			done
			if [ -d "$mkiso_pysrc/lib-dynload" ]; then
				for mkiso_pyso in "$mkiso_pysrc/lib-dynload"/*.so \
					"$mkiso_pysrc/lib-dynload"/*.so.*
				do
					[ -e "$mkiso_pyso" ] || continue
					mkiso_copy_libs "$mkiso_pyso"
				done
			fi
		fi
	fi
}

# Install Meson as meson.py next to mesonbuild so it does not need
# importlib.metadata or a setuptools egg-info (the host /usr/bin/meson
# does). Same layout the meson recipe ships.
mkiso_install_meson()
{
	mkiso_mod=
	mkiso_py_probe=$(mkiso_seed_python) ||
		mkiso_py_probe=/usr/bin/python3
	mkiso_mod=$("$mkiso_py_probe" -c \
		'import mesonbuild, os; print(os.path.dirname(mesonbuild.__file__))' \
		2>/dev/null) || mkiso_mod=
	if [ -z "$mkiso_mod" ] || [ ! -d "$mkiso_mod" ]; then
		for mkiso_cand in \
			/usr/lib/python*/site-packages/mesonbuild \
			/usr/lib64/python*/site-packages/mesonbuild
		do
			[ -d "$mkiso_cand" ] || continue
			mkiso_mod=$mkiso_cand
			break
		done
	fi
	[ -n "$mkiso_mod" ] && [ -d "$mkiso_mod" ] || return 0
	printf '%s: copying Meson from %s\n' "$sps_prog" "$mkiso_mod" >&2
	mkdir -p "$mkiso_live/usr/share/meson" "$mkiso_live/usr/bin"
	rm -rf "$mkiso_live/usr/share/meson/mesonbuild"
	# Homebrew and other foreign layouts symlink module files into a
	# per-version Cellar tree. cp -a preserves those links, and a link
	# whose target points outside the live root dangles once packed.
	# Dereference so the seeded mesonbuild is self-contained.
	cp -aL --reflink=auto "$mkiso_mod" \
		"$mkiso_live/usr/share/meson/mesonbuild" 2>/dev/null ||
		cp -aL "$mkiso_mod" "$mkiso_live/usr/share/meson/mesonbuild" 2>/dev/null ||
		cp -LR "$mkiso_mod" "$mkiso_live/usr/share/meson/mesonbuild"
	cat >"$mkiso_live/usr/share/meson/meson.py" <<'EOF'
#!/usr/bin/python3
import sys
from pathlib import Path
meson_exe = Path(sys.argv[0]).resolve()
if (meson_exe.parent / 'mesonbuild').is_dir():
    sys.path.insert(0, str(meson_exe.parent))
from mesonbuild import mesonmain
if __name__ == '__main__':
    sys.exit(mesonmain.main())
EOF
	chmod 755 "$mkiso_live/usr/share/meson/meson.py"
	rm -f "$mkiso_live/usr/bin/meson"
	ln -s ../share/meson/meson.py "$mkiso_live/usr/bin/meson"
}

mkiso_copy_cmake_data()
{
	printf '%s: copying CMake modules\n' "$sps_prog" >&2
	for mkiso_cmake in /usr/share/cmake /usr/share/cmake-*
	do
		[ -e "$mkiso_cmake" ] || continue
		mkiso_copy_path "$mkiso_cmake"
	done
}

mkiso_copy_perl_lib()
{
	printf '%s: copying Perl library\n' "$sps_prog" >&2
	for mkiso_perl in /usr/lib64/perl5 /usr/lib/perl5 \
		/usr/share/perl5 /usr/share/perl
	do
		[ -e "$mkiso_perl" ] || continue
		mkiso_copy_path "$mkiso_perl"
	done
}

# Host /usr/include is copied so gcc can compile against libc. Optional
# third-party headers on that tree make configure detect libraries the
# disc does not ship (alsa-utils vs samplerate.h) or compile a recipe
# against a different upstream version (host libxml2 vs the tarball).
mkiso_prune_seed_headers()
{
	rm -f "$mkiso_live/usr/include/samplerate.h"
	rm -rf "$mkiso_live/usr/include/libxml2"
}

mkiso_copy_mesa_deps()
{
	printf '%s: resolving Mesa DRI/GBM libraries\n' "$sps_prog" >&2
	for mkiso_d in /usr/lib64/dri /usr/lib/dri /usr/lib64/gbm /usr/lib/gbm
	do
		[ -d "$mkiso_d" ] || continue
		find "$mkiso_d" -type f -name '*.so*' -print 2>/dev/null |
		while IFS= read -r mkiso_so || [ -n "$mkiso_so" ]; do
			[ -n "$mkiso_so" ] || continue
			mkiso_copy_so_deps "$mkiso_so"
		done
	done
	for mkiso_so in /usr/lib64/libgallium*.so /usr/lib64/libgallium*.so.* \
		/usr/lib64/libEGL_mesa.so /usr/lib64/libEGL_mesa.so.* \
		/usr/lib64/libGLX_mesa.so /usr/lib64/libGLX_mesa.so.* \
		/usr/lib64/libgbm.so /usr/lib64/libgbm.so.* \
		/usr/lib64/libglapi.so /usr/lib64/libglapi.so.* \
		/usr/lib64/libSPIRV-Tools.so \
		/usr/lib64/qt6/plugins/platforms/libqwayland.so \
		/usr/lib64/qt6/plugins/platforms/libqxcb.so \
		/usr/lib64/qt6/plugins/wayland-shell-integration/libxdg-shell.so \
		/usr/bin/kwin_wayland /usr/bin/kwin_wayland_wrapper \
		/usr/bin/Xwayland
	do
		[ -e "$mkiso_so" ] || [ -L "$mkiso_so" ] || continue
		mkiso_copy_so_deps "$mkiso_so"
	done
	# Live uses nouveau. A dangling nvidia GBM backend makes libgbm
	# probe a proprietary stack that is not on the disc.
	rm -f "$mkiso_live/usr/lib64/gbm/nvidia-drm_gbm.so" \
		"$mkiso_live/usr/lib/gbm/nvidia-drm_gbm.so"
}

# Session helpers and Plasma's own QML/applet plugins. Tree copies leave the
# plugin .so files in place while their non-Qt libraries (taskmanager,
# KExiv2, notificationmanager, processcore, and others) stay on the host.
mkiso_copy_plasma_runtime_deps()
{
	printf '%s: resolving Plasma session libraries\n' "$sps_prog" >&2
	mkiso_plasma_runtime=$mkiso_work/plasma-runtime-elfs
	: >"$mkiso_plasma_runtime"
	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/bin/xkbcomp \
		/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 \
		/usr/lib64/qt6/plugins/sqldrivers/libqsqlite.so \
		/usr/lib64/qt6/qml/org/kde/kirigami/libKirigamiplugin.so \
		/usr/lib64/qt6/plugins/kf6/kauth/backend/kauth_backend_plugin.so \
		/usr/lib64/qt6/plugins/kwin/plugins/eis.so \
		/usr/lib64/libtaskmanager.so.6 \
		/usr/lib64/libKirigami.so.6 \
		/usr/lib64/libKirigamiPlatform.so.6 \
		/usr/lib64/firefox/firefox-bin \
		/usr/lib64/firefox/libxul.so \
		/usr/lib/qt6/plugins/sqldrivers/libqsqlite.so \
		/usr/lib/qt6/qml/org/kde/kirigami/libKirigamiplugin.so \
		/usr/lib/qt6/plugins/kf6/kauth/backend/kauth_backend_plugin.so \
		/usr/lib/qt6/plugins/kwin/plugins/eis.so
	do
		printf '%s\n' "$mkiso_so" >>"$mkiso_plasma_runtime"
	done
	# org/kde covers Kirigami, activityswitcher, and the rest of the
	# desktop QML graph. plugins/plasma is applets; plugins/kwin is
	# compositor helpers such as eis.so.
	for mkiso_d in \
		/usr/lib64/qt6/qml/org/kde \
		/usr/lib64/qt6/plugins/plasma \
		/usr/lib64/qt6/plugins/kwin \
		/usr/lib64/qt6/plugins/kf6 \
		/usr/lib/qt6/qml/org/kde \
		/usr/lib/qt6/plugins/plasma \
		/usr/lib/qt6/plugins/kwin \
		/usr/lib/qt6/plugins/kf6
	do
		[ -d "$mkiso_d" ] || continue
		find "$mkiso_d" -type f -name '*.so' -print 2>/dev/null \
			>>"$mkiso_plasma_runtime"
	done
	LC_ALL=C sort -u "$mkiso_plasma_runtime" -o "$mkiso_plasma_runtime"
	while IFS= read -r mkiso_so || [ -n "$mkiso_so" ]
	do
		[ -e "$mkiso_so" ] || [ -L "$mkiso_so" ] || continue
		mkiso_copy_so_deps "$mkiso_so"
	done <"$mkiso_plasma_runtime"
}

# 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()
{
	mkiso_host=$1
	[ -e "$mkiso_host" ] || return 0
	command -v ldd >/dev/null 2>&1 || return 0
	mkiso_ldd=$(ldd "$mkiso_host" 2>/dev/null) || return 0
	mkiso_oldifs=$IFS
	IFS='
'
	for mkiso_line in $mkiso_ldd
	do
		IFS=$mkiso_oldifs
		case $mkiso_line in
			*linux-vdso*|*'not a dynamic'*|*'not found'*) continue ;;
		esac
		mkiso_lib=${mkiso_line#*=> }
		mkiso_lib=${mkiso_lib%% (*}
		mkiso_lib=${mkiso_lib## }
		case $mkiso_lib in
			/*)
				if [ ! -e "$mkiso_live$mkiso_lib" ] &&
					[ ! -L "$mkiso_live$mkiso_lib" ]
				then
					sps_die "$SPS_EX_PACKAGE" \
						"live image missing $mkiso_lib (needed by $mkiso_host)"
				fi
				;;
		esac
	done
	IFS=$mkiso_oldifs
}

# Copy a host command by name. live-bins lists common paths, but util-linux
# puts lsblk in /bin on this builder and /usr/bin on others. Silent skips
# of a missing path are why live setup printed "no disks found".
mkiso_copy_host_cmd()
{
	mkiso_cmd=$1
	mkiso_old_path=$PATH
	PATH=/sbin:/usr/sbin:/bin:/usr/bin:$PATH
	mkiso_found=$(command -v "$mkiso_cmd" 2>/dev/null) || mkiso_found=
	PATH=$mkiso_old_path
	[ -n "$mkiso_found" ] && [ -e "$mkiso_found" ] || return 1
	mkiso_copy_path "$mkiso_found"
	mkiso_copy_libs "$mkiso_found"
	return 0
}

# glibc, libtinfo, and the loader. Seed and Plasma both need these; a
# missing libtinfo.so.6 (the real file behind the soname symlink) is what
# made getty respawn `/bin/sh` forever on the Plasma ISO.
mkiso_copy_runtime_libs()
{
	for mkiso_lib in \
		/lib64/ld-linux-x86-64.so.2 \
		/lib/ld-linux-x86-64.so.2 \
		/lib64/libc.so.6 \
		/lib64/libm.so.6 \
		/lib64/libdl.so.2 \
		/lib64/libpthread.so.0 \
		/lib64/librt.so.1 \
		/lib64/libutil.so.1 \
		/lib64/libresolv.so.2 \
		/lib64/libnss_files.so.2 \
		/lib64/libnss_dns.so.2 \
		/lib64/libtinfo.so.6 \
		/lib64/libncursesw.so.6 \
		/lib64/libncurses.so.6 \
		/lib64/libreadline.so.8 \
		/lib64/libgcc_s.so.1 \
		/usr/lib64/libgcc_s.so.1 \
		/usr/lib64/libstdc++.so.6
	do
		[ -e "$mkiso_lib" ] || [ -L "$mkiso_lib" ] || continue
		mkiso_copy_path "$mkiso_lib"
	done
	for mkiso_lib in /lib64/libtinfo.so.* /usr/lib64/libtinfo.so.* \
		/lib64/libncursesw.so.* /lib64/libc.so.* \
		/lib64/ld-linux-x86-64.so.*
	do
		[ -e "$mkiso_lib" ] || [ -L "$mkiso_lib" ] || continue
		mkiso_copy_path "$mkiso_lib"
	done
	mkiso_copy_path /etc/ld.so.conf
	mkiso_copy_path /etc/ld.so.conf.d
	if [ ! -f "$mkiso_live/etc/ld.so.conf" ]; then
		mkdir -p "$mkiso_live/etc"
		printf '%s\n' '/lib64' '/usr/lib64' '/lib' '/usr/lib' \
			>"$mkiso_live/etc/ld.so.conf"
	fi
	mkiso_copy_path /usr/share/terminfo
	mkiso_copy_path /etc/terminfo
	mkiso_copy_path /lib/terminfo
}

# ldd live *binaries* so their DT_NEEDED closure is on the disc.
# Do not walk lib directories: a Plasma tree has tens of thousands of
# .so files, and ldd-per-file never finishes (the ISO looks hung after
# "live PID 1 is busybox init"). Plugins are already copied as trees;
# session binaries are ldd'd from plasma-bins.
mkiso_close_libs()
{
	command -v ldd >/dev/null 2>&1 || return 0
	printf '%s: resolving shared libraries for live binaries\n' \
		"$sps_prog" >&2
	mkiso_find=
	for mkiso_d in bin sbin usr/bin usr/sbin
	do
		[ -d "$mkiso_live/$mkiso_d" ] || continue
		mkiso_find="$mkiso_find $mkiso_live/$mkiso_d"
	done
	[ -n "$mkiso_find" ] || return 0
	# shellcheck disable=SC2086
	find $mkiso_find -type f \( -perm -0100 -o -perm -0010 -o -perm -0001 \) \
		-print 2>/dev/null |
	while IFS= read -r mkiso_elf || [ -n "$mkiso_elf" ]; do
		[ -n "$mkiso_elf" ] || continue
		# Scripts and data are not DT_NEEDED sources.
		mkiso_magic=$(dd if="$mkiso_elf" bs=4 count=1 2>/dev/null) || continue
		case $mkiso_magic in
			*$'\x7fELF'*) mkiso_copy_libs "$mkiso_elf" ;;
		esac
	done
}

mkiso_ldconfig_live()
{
	command -v ldconfig >/dev/null 2>&1 || return 0
	ldconfig -r "$mkiso_live" 2>/dev/null || :
}

# Getty and live-rc exec /bin/sh. That must be static busybox, never bash.
mkiso_force_busybox_sh()
{
	[ -x "$mkiso_live/bin/busybox" ] || return 0
	ln -sf busybox "$mkiso_live/bin/sh"
}

mkiso_stamp_os_release()
{
	mkdir -p "$mkiso_live/etc"
	if [ -n "$mkiso_extra" ] &&
		[ -f "$mkiso_extra/base/splux-base/files/os-release" ]
	then
		cp "$mkiso_extra/base/splux-base/files/os-release" \
			"$mkiso_live/etc/os-release"
		return 0
	fi
	printf '%s\n' \
		'NAME="Splux"' \
		'PRETTY_NAME="Splux Linux"' \
		'ID=splux' \
		'VERSION="1.0"' \
		'VERSION_ID="1.0"' \
		'LOGO=splux' \
		'ANSI_COLOR="1;33"' \
		'HOME_URL="https://splux.robertflexx.dev"' \
		'DOCUMENTATION_URL="https://splux.robertflexx.dev/docs/"' \
		'SUPPORT_URL="https://github.com/RobertFlexx/SPS"' \
		'BUG_REPORT_URL="https://github.com/RobertFlexx/SPS/issues"' \
		>"$mkiso_live/etc/os-release"
}

# 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=
	if [ -n "$mkiso_extra" ] &&
		[ -d "$mkiso_extra/base/splux-base/files" ]
	then
		mkiso_brand=$mkiso_extra/base/splux-base/files
	fi

	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 [ -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 [ -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 [ -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
# "error while loading shared libraries: libtinfo.so.6".
mkiso_verify_live()
{
	[ -x "$mkiso_live/bin/busybox" ] || return 0
	if [ -L "$mkiso_live/bin/sh" ]; then
		mkiso_sh=$(readlink "$mkiso_live/bin/sh")
		case $mkiso_sh in
			busybox|./busybox|/bin/busybox) ;;
			*)
				sps_die "$SPS_EX_BUILD" \
					"live /bin/sh must be busybox (got $mkiso_sh)"
				;;
		esac
	else
		sps_die "$SPS_EX_BUILD" "live /bin/sh is not a busybox symlink"
	fi

	mkiso_interp=
	for mkiso_p in "$mkiso_live/lib64/ld-linux-x86-64.so.2" \
		"$mkiso_live/lib/ld-linux-x86-64.so.2" \
		"$mkiso_live/usr/lib64/ld-linux-x86-64.so.2"
	do
		if [ -e "$mkiso_p" ]; then
			mkiso_interp=$mkiso_p
			break
		fi
	done
	mkiso_lp=$mkiso_live/lib64:$mkiso_live/usr/lib64:$mkiso_live/lib:$mkiso_live/usr/lib

	if [ -x "$mkiso_live/bin/bash" ] && [ ! -L "$mkiso_live/bin/bash" ]; then
		if [ ! -e "$mkiso_live/lib64/libtinfo.so.6" ] &&
			[ ! -e "$mkiso_live/usr/lib64/libtinfo.so.6" ]; then
			sps_die "$SPS_EX_BUILD" \
				"live bash is present but libtinfo.so.6 is not"
		fi
		if [ -n "$mkiso_interp" ]; then
			if ! "$mkiso_interp" --library-path "$mkiso_lp" \
				"$mkiso_live/bin/bash" -c ':' >/dev/null 2>&1; then
				sps_die "$SPS_EX_BUILD" \
					"live /bin/bash cannot execute (missing libraries)"
			fi
		fi
	fi

	# chroot needs privileges; skip it when the kernel refuses the
	# syscall (user namespaces, unprivileged root in tests).
	if [ "$(id -u 2>/dev/null)" = 0 ]; then
		mkiso_chroot_st=0
		mkiso_chroot_err=$("$mkiso_live/bin/busybox" chroot \
			"$mkiso_live" /bin/sh -c ':' 2>&1) || mkiso_chroot_st=$?
		if [ "$mkiso_chroot_st" -ne 0 ]; then
			case $mkiso_chroot_err in
				*'Operation not permitted'*|*'not permitted'*) ;;
				*)
					sps_die "$SPS_EX_BUILD" \
						"live /bin/sh cannot execute inside the image"
					;;
			esac
		fi
	fi

	if [ -e /usr/sbin/efibootmgr ] || [ -L /usr/sbin/efibootmgr ] ||
		[ -e /usr/bin/efibootmgr ] || [ -L /usr/bin/efibootmgr ]
	then
		if [ ! -e "$mkiso_live/usr/sbin/efibootmgr" ] &&
			[ ! -L "$mkiso_live/usr/sbin/efibootmgr" ] &&
			[ ! -e "$mkiso_live/usr/bin/efibootmgr" ] &&
			[ ! -L "$mkiso_live/usr/bin/efibootmgr" ]
		then
			sps_die "$SPS_EX_PACKAGE" \
				"seeded live image is missing efibootmgr"
		fi
	fi

	mkiso_verify_compiler
	mkiso_verify_plasma
	mkiso_verify_setup_catalog
}

# Seeded images ship gcc. The driver DT_NEEDs only libc; cc1 needs
# libmpc/libisl. Refuse to ship a disc whose first autoconf check dies.
mkiso_verify_compiler()
{
	if [ ! -e "$mkiso_live/usr/bin/gcc" ] &&
		[ ! -L "$mkiso_live/usr/bin/gcc" ]
	then
		return 0
	fi
	if [ ! -e "$mkiso_live/usr/lib64/crt1.o" ] &&
		[ ! -e "$mkiso_live/lib64/crt1.o" ]
	then
		sps_die "$SPS_EX_PACKAGE" \
			"seeded live gcc is missing crt1.o"
	fi
	if [ ! -e "$mkiso_live/usr/lib64/libc.so" ] &&
		[ ! -L "$mkiso_live/usr/lib64/libc.so" ]
	then
		sps_die "$SPS_EX_PACKAGE" \
			"seeded live gcc is missing the libc.so linker script"
	fi
	# glibc 2.34 folded pthread into libc. On such hosts -lpthread is
	# provided by a tiny compatibility archive rather than libpthread.so.
	# Autoconf's ALSA probe still passes -lpthread explicitly.
	if [ -e /usr/lib64/libpthread.a ] &&
		[ ! -e "$mkiso_live/usr/lib64/libpthread.a" ]
	then
		sps_die "$SPS_EX_PACKAGE" \
			"seeded live gcc is missing the libpthread.a linker archive"
	fi
	if [ -e /usr/bin/msgfmt ] &&
		[ ! -e "$mkiso_live/usr/bin/msgfmt" ] &&
		[ ! -L "$mkiso_live/usr/bin/msgfmt" ]
	then
		sps_die "$SPS_EX_PACKAGE" \
			"seeded live gcc is missing msgfmt"
	fi
	if [ ! -e "$mkiso_live/usr/bin/as" ] &&
		[ ! -L "$mkiso_live/usr/bin/as" ]
	then
		sps_die "$SPS_EX_PACKAGE" "seeded live gcc is missing as"
	fi
	if [ ! -e "$mkiso_live/usr/bin/ld" ] &&
		[ ! -L "$mkiso_live/usr/bin/ld" ]
	then
		sps_die "$SPS_EX_PACKAGE" "seeded live gcc is missing ld"
	fi
	mkiso_cc1=
	for mkiso_d in "$mkiso_live/usr/libexec/gcc" "$mkiso_live/usr/lib64/gcc"
	do
		[ -d "$mkiso_d" ] || continue
		mkiso_cc1=$(find "$mkiso_d" -name cc1 -type f -print 2>/dev/null |
			head -1) || mkiso_cc1=
		[ -n "$mkiso_cc1" ] && break
	done
	[ -n "$mkiso_cc1" ] ||
		sps_die "$SPS_EX_PACKAGE" "seeded live gcc is missing cc1"
	mkiso_verify_elf_in_live "${mkiso_cc1#"$mkiso_live"}"
	mkiso_verify_elf_in_live /usr/bin/as
	if [ -e /usr/bin/ld.bfd ]; then
		mkiso_verify_elf_in_live /usr/bin/ld.bfd
	elif [ -e /usr/bin/ld ]; then
		mkiso_verify_elf_in_live /usr/bin/ld
	fi
	# Meson is a Python program. The host driver is a setuptools
	# wrapper; without mesonbuild and encodings, the first meson
	# recipe dies with a traceback (libxml2's -Dpython= option is a
	# second, separate failure).
	if [ -e /usr/bin/meson ] || [ -L /usr/bin/meson ]; then
		if [ ! -e "$mkiso_live/usr/share/meson/mesonbuild/mesonmain.py" ]
		then
			sps_die "$SPS_EX_PACKAGE" \
				"seeded meson is missing mesonbuild"
		fi
		if [ ! -e "$mkiso_live/usr/bin/meson" ] &&
			[ ! -L "$mkiso_live/usr/bin/meson" ]
		then
			sps_die "$SPS_EX_PACKAGE" \
				"seeded meson wrapper is missing"
		fi
		mkiso_pyver=$(mkiso_python_version)
		if [ ! -e "$mkiso_live/usr/lib64/python$mkiso_pyver/encodings/__init__.py" ] &&
			[ ! -e "$mkiso_live/usr/lib/python$mkiso_pyver/encodings/__init__.py" ]
		then
			sps_die "$SPS_EX_PACKAGE" \
				"seeded python is missing the encodings stdlib module"
		fi
	fi
	if [ -e /usr/bin/pod2man ] || [ -L /usr/bin/pod2man ]; then
		if [ ! -e "$mkiso_live/usr/bin/pod2man" ] &&
			[ ! -L "$mkiso_live/usr/bin/pod2man" ]
		then
			sps_die "$SPS_EX_PACKAGE" \
				"seeded live gcc is missing pod2man"
		fi
	fi
}

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/bin/plasmashell usr/bin/xkbcomp \
		usr/libexec/kactivitymanagerd usr/libexec/kscreenlocker_greet \
		usr/lib64/gbm/dri_gbm.so usr/lib64/libxcb-cursor.so.0 \
		usr/lib64/libtaskmanager.so.6 usr/lib64/libKirigami.so.6 \
		usr/lib64/qt6/plugins/sqldrivers/libqsqlite.so \
		usr/lib64/qt6/qml/org/kde/kirigami/libKirigamiplugin.so \
		usr/share/xkeyboard-config-2/rules/evdev \
		usr/share/libinput/10-generic-keyboard.quirks \
		usr/share/icons/breeze_cursors/index.theme \
		usr/share/systemsettings/categories \
		usr/share/glib-2.0/schemas/gschemas.compiled \
		usr/lib/polkit-1/polkitd \
		etc/xdg/kscreenlockerrc
	do
		if [ ! -e "$mkiso_live/$mkiso_need" ] &&
			[ ! -L "$mkiso_live/$mkiso_need" ]
		then
			mkiso_miss="$mkiso_miss $mkiso_need"
		fi
	done
	if [ -e /usr/bin/firefox ]; then
		if [ ! -e "$mkiso_live/usr/bin/firefox" ] &&
			[ ! -L "$mkiso_live/usr/bin/firefox" ]
		then
			mkiso_miss="$mkiso_miss usr/bin/firefox"
		fi
		if [ ! -e "$mkiso_live/usr/lib64/firefox/firefox-bin" ] &&
			[ ! -L "$mkiso_live/usr/lib64/firefox/firefox-bin" ]
		then
			mkiso_miss="$mkiso_miss usr/lib64/firefox/firefox-bin"
		fi
	fi
	if ! ls "$mkiso_live"/usr/lib64/libgallium*.so* >/dev/null 2>&1
	then
		mkiso_miss="$mkiso_miss libgallium"
	fi
	if [ -e /usr/lib64/libSPIRV-Tools.so ] ||
		[ -L /usr/lib64/libSPIRV-Tools.so ]
	then
		if ! ls "$mkiso_live"/usr/lib64/libSPIRV-Tools.so* >/dev/null 2>&1
		then
			mkiso_miss="$mkiso_miss libSPIRV-Tools.so"
		fi
	fi
	if [ -e /usr/bin/Xwayland ] &&
		[ ! -e "$mkiso_live/usr/bin/Xwayland" ] &&
		[ ! -L "$mkiso_live/usr/bin/Xwayland" ]
	then
		mkiso_miss="$mkiso_miss usr/bin/Xwayland"
	fi
	if ! find "$mkiso_live/usr/share/fonts" -type f -name '*.ttf' \
		-print -quit 2>/dev/null | grep -q .
	then
		mkiso_miss="$mkiso_miss TrueType fonts"
	fi
	[ -z "$mkiso_miss" ] ||
		sps_die "$SPS_EX_PACKAGE" "plasma image missing:$mkiso_miss"
	for mkiso_g in /usr/lib64/libgallium*.so
	do
		[ -e "$mkiso_g" ] || continue
		mkiso_verify_elf_in_live "$mkiso_g"
		break
	done
	if [ -f "$mkiso_work/plasma-runtime-elfs" ]; then
		while IFS= read -r mkiso_plugin || [ -n "$mkiso_plugin" ]
		do
			[ -e "$mkiso_plugin" ] || [ -L "$mkiso_plugin" ] || continue
			mkiso_verify_elf_in_live "$mkiso_plugin"
		done <"$mkiso_work/plasma-runtime-elfs"
	fi
}

# setup extras/sets/profiles may only name packages that exist in the
# recipe trees on this image. An extras checklist entry with no matching
# recipe is how live setup died at "package 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()
{
	mkiso_bbdir=$1
	[ -x "$mkiso_bbdir/busybox" ] || return 1
	"$mkiso_bbdir/busybox" --list | while IFS= read -r mkiso_ap || [ -n "$mkiso_ap" ]
	do
		[ -n "$mkiso_ap" ] || continue
		[ "$mkiso_ap" = busybox ] && continue
		if [ -e "$mkiso_bbdir/$mkiso_ap" ] &&
			[ ! -L "$mkiso_bbdir/$mkiso_ap" ]
		then
			continue
		fi
		ln -sf busybox "$mkiso_bbdir/$mkiso_ap"
	done
	ln -sf busybox "$mkiso_bbdir/sh"
}

# filesystem ships /bin -> usr/bin. A real /bin directory on the live
# root makes pkin refuse the package, so sget install dies immediately.
mkiso_ensure_usr_merge()
{
	mkdir -p "$mkiso_live/usr/bin" "$mkiso_live/usr/sbin" \
		"$mkiso_live/usr/lib"
	for mkiso_merge in bin:usr/bin sbin:usr/sbin lib:usr/lib
	do
		mkiso_merge_name=${mkiso_merge%%:*}
		mkiso_merge_target=${mkiso_merge#*:}
		mkiso_merge_path=$mkiso_live/$mkiso_merge_name
		if [ -L "$mkiso_merge_path" ]; then
			continue
		fi
		if [ -e "$mkiso_merge_path" ]; then
			sps_die "$SPS_EX_BUILD" \
				"live /$mkiso_merge_name exists and is not a usr-merge symlink"
		fi
		ln -s "$mkiso_merge_target" "$mkiso_merge_path"
	done
}

# Own the usr-merge links and static files so later packages that depend
# on filesystem can install on the live image itself.
mkiso_install_base_filesystem()
{
	mkiso_fs_recipe=
	if [ -n "${mkiso_core:-}" ] &&
		[ -f "$mkiso_core/base/filesystem/recipe" ]
	then
		mkiso_fs_recipe=$mkiso_core/base/filesystem/recipe
	elif [ -f "$mkiso_libdir/filesystem.recipe" ]; then
		mkiso_fs_recipe=$mkiso_libdir/filesystem.recipe
	fi
	[ -n "$mkiso_fs_recipe" ] || return 0

	printf '%s: installing filesystem into the live root\n' \
		"$sps_prog" >&2
	mkiso_fs_out=$(mktemp -d "${TMPDIR:-/tmp}/.sps-mkiso-fs.XXXXXX") ||
		sps_die "$SPS_EX_BUILD" "cannot create filesystem package workspace"
	mkiso_fs_st=0
	if ! (
		unset SPS_ROOT SPS_DB SPS_CACHE SPS_BUILD SPS_CONFIG \
			SPS_REPOS_CONFIG SPS_COMPRESSION
		SPS_ROOT=$mkiso_live
		SPS_DB=$mkiso_live/var/lib/sps
		SPS_CACHE=$mkiso_live/var/cache/sps
		SPS_BUILD=$mkiso_live/var/tmp/sps
		SPS_CONFIG=/dev/null
		SPS_REPOS_CONFIG=/dev/null
		SPS_COMPRESSION=none
		PATH=$sps_bin_dir:$PATH
		export SPS_ROOT SPS_DB SPS_CACHE SPS_BUILD SPS_CONFIG \
			SPS_REPOS_CONFIG SPS_COMPRESSION SPS_LIBDIR PATH
		mkdir -p "$SPS_DB" "$SPS_CACHE" "$SPS_BUILD"
		mkpkg --output "$mkiso_fs_out" "$mkiso_fs_recipe" || exit 1
		mkiso_fs_pkg=
		for mkiso_fs_cand in "$mkiso_fs_out"/filesystem-*.pkg.tar \
			"$mkiso_fs_out"/filesystem-*.pkg.tar.gz \
			"$mkiso_fs_out"/filesystem-*.pkg.tar.xz \
			"$mkiso_fs_out"/filesystem-*.pkg.tar.zst
		do
			[ -f "$mkiso_fs_cand" ] || continue
			mkiso_fs_pkg=$mkiso_fs_cand
			break
		done
		[ -n "$mkiso_fs_pkg" ] || exit 1
		pkin --dependency "$mkiso_fs_pkg"
	)
	then
		mkiso_fs_st=1
	fi
	rm -rf "$mkiso_fs_out"
	[ "$mkiso_fs_st" -eq 0 ] ||
		sps_die "$SPS_EX_BUILD" \
			"failed to install filesystem into the live root"
}

mkiso_add_group()
{
	mkiso_gname=$1
	mkiso_gid=$2
	mkiso_gmembers=${3-}
	mkiso_gfile=$mkiso_live/etc/group
	[ -f "$mkiso_gfile" ] || printf '%s\n' 'root:x:0:' >"$mkiso_gfile"
	grep -q "^${mkiso_gname}:" "$mkiso_gfile" && return 0
	if [ -n "$mkiso_gmembers" ]; then
		printf '%s:x:%s:%s\n' "$mkiso_gname" "$mkiso_gid" \
			"$mkiso_gmembers" >>"$mkiso_gfile"
	else
		printf '%s:x:%s:\n' "$mkiso_gname" "$mkiso_gid" >>"$mkiso_gfile"
	fi
}

mkiso_add_user()
{
	mkiso_uname=$1
	mkiso_uid=$2
	mkiso_ugid=$3
	mkiso_ugeos=$4
	mkiso_uhome=$5
	mkiso_pfile=$mkiso_live/etc/passwd
	mkiso_sfile=$mkiso_live/etc/shadow
	[ -f "$mkiso_pfile" ] ||
		printf '%s\n' 'root:x:0:0:root:/root:/bin/sh' >"$mkiso_pfile"
	grep -q "^${mkiso_uname}:" "$mkiso_pfile" && return 0
	printf '%s:x:%s:%s:%s:%s:/usr/bin/nologin\n' \
		"$mkiso_uname" "$mkiso_uid" "$mkiso_ugid" "$mkiso_ugeos" \
		"$mkiso_uhome" >>"$mkiso_pfile"
	if [ -f "$mkiso_sfile" ] && ! grep -q "^${mkiso_uname}:" "$mkiso_sfile"
	then
		printf '%s:!:0:0:99999:7:::\n' "$mkiso_uname" >>"$mkiso_sfile"
	fi
}

mkiso_write_live_accounts()
{
	mkdir -p "$mkiso_live/etc" "$mkiso_live/var/lib/polkit" \
		"$mkiso_live/run/dbus"
	if [ ! -s "$mkiso_live/etc/passwd" ]; then
		printf '%s\n' 'root:x:0:0:root:/root:/bin/sh' \
			>"$mkiso_live/etc/passwd"
	fi
	if [ ! -s "$mkiso_live/etc/group" ]; then
		printf '%s\n' 'root:x:0:' >"$mkiso_live/etc/group"
	fi
	mkiso_add_group tty 5
	mkiso_add_group disk 6
	mkiso_add_group audio 17
	mkiso_add_group video 18 root
	mkiso_add_group input 71 root
	mkiso_add_group render 107 root
	mkiso_add_group messagebus 81
	mkiso_add_group polkitd 87
	mkiso_add_user messagebus 81 81 "User for D-BUS" /run/dbus
	mkiso_add_user polkitd 87 87 "PolicyKit daemon owner" /var/lib/polkit
	if [ ! -s "$mkiso_live/etc/hosts" ]; then
		printf '%s\n' '127.0.0.1 localhost' '::1 localhost' \
			>"$mkiso_live/etc/hosts"
	fi
	if ! grep -q splux-live "$mkiso_live/etc/hosts"; then
		mkiso_hosts=$mkiso_live/etc/hosts
		mkiso_hosts_tmp=$(mktemp "${TMPDIR:-/tmp}/.sps-hosts.XXXXXX") ||
			sps_die "$SPS_EX_BUILD" "cannot stage live hosts"
		awk '{
			if ($1 == "127.0.0.1" && $0 !~ /splux-live/)
				print $0 " splux-live"
			else if ($1 == "::1" && $0 !~ /splux-live/)
				print $0 " splux-live"
			else
				print
		}' "$mkiso_hosts" >"$mkiso_hosts_tmp"
		cat "$mkiso_hosts_tmp" >"$mkiso_hosts"
		rm -f "$mkiso_hosts_tmp"
	fi
	if [ ! -s "$mkiso_live/etc/nsswitch.conf" ]; then
		printf '%s\n' \
			'passwd: files' \
			'group: files' \
			'shadow: files' \
			'hosts: files dns' \
			'networks: files' \
			'protocols: files' \
			'services: files' \
			'ethers: files' \
			'rpc: files' \
			>"$mkiso_live/etc/nsswitch.conf"
	fi
	if [ ! -s "$mkiso_live/etc/shadow" ]; then
		printf '%s\n' 'root::0:0:99999:7:::' \
			>"$mkiso_live/etc/shadow"
	else
		mkiso_shadow_tmp=$(mktemp "${TMPDIR:-/tmp}/.sps-shadow.XXXXXX") ||
			sps_die "$SPS_EX_BUILD" "cannot stage live shadow"
		awk -F: 'BEGIN { OFS=":" }
			$1 == "root" { $2 = ""; print; next }
			{ print }' "$mkiso_live/etc/shadow" >"$mkiso_shadow_tmp"
		cat "$mkiso_shadow_tmp" >"$mkiso_live/etc/shadow"
		rm -f "$mkiso_shadow_tmp"
	fi
	chmod 600 "$mkiso_live/etc/shadow"
	if [ -f "$mkiso_live/etc/gshadow" ]; then
		chmod 600 "$mkiso_live/etc/gshadow"
	fi
}

# 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
			printf '%s: copying %s\n' "$sps_prog" "$mkiso_tree" >&2
			mkiso_copy_path "$mkiso_tree"
		done <"$mkiso_libdir/plasma-trees"
	fi
	# Shared objects the session links by soname. Copy the files; do not
	# ldd each one (this host has hundreds of Qt/KF libraries, and that
	# is what made plasma ISO creation look hung after the tree copies).
	# plasma-bins and close_libs still pull DT_NEEDED closures.
	printf '%s: copying Qt/KF shared libraries\n' "$sps_prog" >&2
	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* \
		/usr/lib64/libEGL_mesa.so* /usr/lib64/libGLX_mesa.so* \
		/usr/lib64/libgallium*.so* /usr/lib64/dri/libdril_dri.so \
		/usr/lib64/libLLVM.so /usr/lib64/libLLVM.so.[0-9]* \
		/usr/lib64/libOpenGL.so* /usr/lib64/libxcb-cursor.so* \
		/usr/lib64/libgbm.so* /usr/lib64/libglapi.so* \
		/usr/lib64/libSPIRV-Tools.so
	do
		for mkiso_so in $mkiso_pat; do
			[ -e "$mkiso_so" ] || continue
			# Skip the rust-wrapped LLVM; the Mesa DRI stack uses libLLVM.so.*
			case $mkiso_so in
				*-rust*) continue ;;
			esac
			mkiso_copy_path "$mkiso_so"
		done
	done
	# 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 \
		/usr/share/fonts/X11/dejavu/DejaVu*.ttf \
		/usr/share/fonts/X11/liberation-fonts/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
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
	# left dbus-daemon --system looking for a messagebus account the live
	# passwd does not have.
	mkdir -p "$mkiso_live/etc/dbus-1"
	cat >"$mkiso_live/etc/dbus-1/system-local.conf" <<'EOF'
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
  <user>root</user>
</busconfig>
EOF
	for mkiso_cmd in dbus-daemon dbus-run-session dbus-launch dbus-uuidgen \
		dbus-send
	do
		mkiso_copy_host_cmd "$mkiso_cmd" || :
	done
	mkiso_copy_path /usr/libexec/dbus-daemon-launch-helper
	# startplasmacompositor looks up `qdbus` on PATH. This host ships qdbus6.
	if [ -e "$mkiso_live/usr/bin/qdbus6" ] &&
		[ ! -e "$mkiso_live/usr/bin/qdbus" ]
	then
		ln -sf qdbus6 "$mkiso_live/usr/bin/qdbus"
	fi
	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
}

# 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"
	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"

	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"
		mkiso_force_busybox_sh
		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"
	elif [ "$mkiso_init" = systemd ] || [ "$mkiso_init" = openrc ]; then
		# Falling back silently ships a disc that says systemd in
		# /etc/sps/init and on the kernel command line while PID 1 is
		# really busybox. Every service the image declares is then
		# quietly dead and the cause is invisible from inside the
		# booted system. Refuse instead.
		sps_die "$SPS_EX_PACKAGE" \
			"--init $mkiso_init was requested but this host has no $mkiso_init binaries to copy"
	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"
	mkiso_verify_init
}

# /sbin/init existing is not the same as /sbin/init being runnable. A dangling
# symlink, a missing program interpreter, or a missing shared library all make
# the kernel fail switch_root with ENOENT, which it reports as "can't execute
# /sbin/init: No such file or directory". That panic is only visible after the
# disc is written and booted, so check it here while it is still cheap.
mkiso_verify_init()
{
	mkiso_init_target=$mkiso_live/sbin/init
	mkiso_init_hops=0
	while [ -L "$mkiso_init_target" ]; do
		mkiso_init_hops=$((mkiso_init_hops + 1))
		[ "$mkiso_init_hops" -lt 10 ] ||
			sps_die "$SPS_EX_PACKAGE" "/sbin/init is a symlink loop"
		mkiso_init_link=$(readlink "$mkiso_init_target")
		case $mkiso_init_link in
			/*) mkiso_init_target=$mkiso_live$mkiso_init_link ;;
			*) mkiso_init_target=${mkiso_init_target%/*}/$mkiso_init_link ;;
		esac
	done
	[ -f "$mkiso_init_target" ] ||
		sps_die "$SPS_EX_PACKAGE" \
			"/sbin/init resolves to ${mkiso_init_target#$mkiso_live}, which is not in the live root"
	[ -x "$mkiso_init_target" ] ||
		sps_die "$SPS_EX_PACKAGE" "/sbin/init is not executable"

	command -v readelf >/dev/null 2>&1 || return 0
	# readelf prints "  [     0]  /lib64/ld-linux-x86-64.so.2"; take the
	# whole path after the offset field rather than matching on "ld-",
	# which a greedy expression would clip to the last path component.
	mkiso_init_interp=$(readelf -p .interp "$mkiso_init_target" 2>/dev/null |
		sed -n 's/^[[:space:]]*\[[^]]*\][[:space:]]*\(\/.*\)$/\1/p' |
		head -n 1)
	if [ -n "$mkiso_init_interp" ] &&
		[ ! -e "$mkiso_live$mkiso_init_interp" ]
	then
		sps_die "$SPS_EX_PACKAGE" \
			"PID 1 needs interpreter $mkiso_init_interp, which is not in the live root"
	fi

	command -v ldd >/dev/null 2>&1 || return 0
	mkiso_init_missing=$(ldd "$mkiso_init_target" 2>/dev/null |
		sed -n 's/^[[:space:]]*\([^ ]*\) => not found.*/\1/p')
	[ -z "$mkiso_init_missing" ] ||
		printf '%s: warning: PID 1 has unresolved libraries on the host: %s\n' \
			"$sps_prog" "$(printf '%s' "$mkiso_init_missing" | tr '\n' ' ')" >&2
}

# --- live root contents -------------------------------------------------

printf '%s\n' "mkiso: installing SPS tools into the live root" >&2
make -C "$sps_bin_dir/.." \
	DESTDIR="$mkiso_live" PREFIX=/usr \
	install install-config >/dev/null ||
	sps_die "$SPS_EX_BUILD" "make install into the live root failed"

mkdir -p "$mkiso_live/usr/bin" "$mkiso_live/usr/sbin" "$mkiso_live/root" \
	"$mkiso_live/mnt" "$mkiso_live/proc" "$mkiso_live/sys" \
	"$mkiso_live/dev" "$mkiso_live/tmp" "$mkiso_live/run" \
	"$mkiso_live/etc/sps" "$mkiso_live/usr/src/sps" \
	"$mkiso_live/var/cache/sps" "$mkiso_live/var/lib/sps" \
	"$mkiso_live/var/tmp/sps"
mkiso_install_base_filesystem
mkiso_ensure_usr_merge

if [ -z "$mkiso_busybox" ]; then
	for mkiso_bb in /tmp/sps-wave/busybox.static \
		"$mkiso_libdir/busybox" /usr/bin/busybox /bin/busybox
	do
		if [ -x "$mkiso_bb" ]; then
			mkiso_busybox=$mkiso_bb
			break
		fi
	done
fi
[ -n "$mkiso_busybox" ] && [ -x "$mkiso_busybox" ] || {
	if [ -n "$mkiso_output" ]; then
		sps_die "$SPS_EX_PACKAGE" \
			"busybox is required for an ISO (pass --busybox FILE)"
	fi
}

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"
	mkiso_force_busybox_sh
	ln -sf ../bin/busybox "$mkiso_live/sbin/busybox" 2>/dev/null || :
fi

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' splux-live >"$mkiso_live/etc/hostname"
mkiso_stamp_os_release
mkiso_write_live_accounts
# Do not copy the host nsswitch.conf: this builder uses mdns modules the
# live disc does not ship. Without a file, glibc still looks at 127.0.0.1
# when resolv.conf is missing, so live DNS looks like it "does not exist".
if [ ! -s "$mkiso_live/etc/nsswitch.conf" ]; then
	printf '%s\n' \
		'passwd: files' \
		'group: files' \
		'shadow: files' \
		'hosts: files dns' \
		'networks: files' \
		'protocols: files' \
		'services: files' \
		'ethers: files' \
		'rpc: files' \
		>"$mkiso_live/etc/nsswitch.conf"
fi
printf '%s\n' \
	'nameserver 1.1.1.1' \
	'nameserver 8.8.8.8' \
	>"$mkiso_live/etc/resolv.conf"
if [ -f "$mkiso_libdir/udhcpc-script" ]; then
	install -m 0755 "$mkiso_libdir/udhcpc-script" \
		"$mkiso_live/etc/sps/udhcpc-script"
fi

sps_print_default_conf >"$mkiso_live/etc/sps/sps.conf"

mkiso_repos=$mkiso_live/etc/sps/repos.conf
: >"$mkiso_repos"

if [ -n "$mkiso_core" ] && [ -d "$mkiso_core" ]; then
	printf '%s: copying sps-core recipes\n' "$sps_prog" >&2
	rm -rf "$mkiso_live/usr/src/sps/core"
	mkdir -p "$mkiso_live/usr/src/sps/core"
	cp -a "$mkiso_core"/. "$mkiso_live/usr/src/sps/core"/
	rm -rf "$mkiso_live/usr/src/sps/core/.git"
	printf 'dir core /usr/src/sps/core 100\n' >>"$mkiso_repos"
fi
if [ -n "$mkiso_extra" ] && [ -d "$mkiso_extra" ]; then
	printf '%s: copying sps-extra recipes\n' "$sps_prog" >&2
	rm -rf "$mkiso_live/usr/src/sps/extra"
	mkdir -p "$mkiso_live/usr/src/sps/extra"
	cp -a "$mkiso_extra"/. "$mkiso_live/usr/src/sps/extra"/
	rm -rf "$mkiso_live/usr/src/sps/extra/.git"
	printf 'dir extra /usr/src/sps/extra 80\n' >>"$mkiso_repos"
fi
if [ ! -s "$mkiso_repos" ]; then
	cat >>"$mkiso_repos" <<'EOF'
git core https://github.com/RobertFlexx/sps-core.git 100
git extra https://github.com/RobertFlexx/sps-extra.git 80
EOF
fi
sps_print_community_repos_hint >>"$mkiso_repos"

# dialog and glibc/libtinfo on every image, including --no-seed.
if [ -f "$mkiso_libdir/live-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/live-bins"
fi
printf '%s: copying disk tools for setup\n' "$sps_prog" >&2
for mkiso_cmd in lsblk blkid sfdisk cfdisk fdisk wipefs mkfs.ext4 mke2fs \
	mkfs.vfat mkswap partprobe udevadm udevd grub-install grub-mkconfig \
	efibootmgr dhcpcd
do
	mkiso_copy_host_cmd "$mkiso_cmd" || :
done
mkiso_copy_runtime_libs
mkiso_force_busybox_sh

if [ "$mkiso_copy_seed" -eq 1 ] && [ -f "$mkiso_libdir/seed-bins" ]; then
	printf '%s: copying host seed tools\n' "$sps_prog" >&2
	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/seed-bins"
	# Compiler support files.
	mkiso_copy_path /usr/libexec/gcc
	mkiso_copy_path /usr/lib64/gcc
	mkiso_copy_path /usr/lib/gcc
	mkiso_copy_path /usr/include
	mkiso_prune_seed_headers
	# gcc and ld without glibc startfiles fail the first autoconf check
	# with "C compiler cannot create executables". Runtime libc.so.6 is
	# not enough; the linker needs crt1.o and the libc.so script.
	printf '%s: copying glibc startfiles for the seed compiler\n' \
		"$sps_prog" >&2
	for mkiso_crt in /usr/lib64/crt1.o /usr/lib64/crti.o /usr/lib64/crtn.o \
		/usr/lib64/Scrt1.o /usr/lib64/gcrt1.o /usr/lib64/Mcrt1.o \
		/lib64/crt1.o /lib64/crti.o /lib64/crtn.o \
		/usr/lib64/libc.so /usr/lib64/libm.so /usr/lib64/libmvec.so \
		/usr/lib64/libpthread.so /usr/lib64/libdl.so \
		/usr/lib64/librt.so /usr/lib64/libutil.so \
		/usr/lib64/libresolv.so /usr/lib64/libanl.so \
		/usr/lib64/libthread_db.so /usr/lib64/libgcc_s.so \
		/usr/lib64/libstdc++.so /usr/lib64/libc_nonshared.a \
		/usr/lib64/libpthread.a /usr/lib64/libdl.a \
		/usr/lib64/librt.a /usr/lib64/libutil.a \
		/usr/lib64/libanl.a \
		/usr/lib64/libpthread_nonshared.a \
		/usr/lib64/libmvec_nonshared.a \
		/usr/lib64/rcrt1.o /usr/lib64/grcrt1.o \
		/lib64/libc.so.6 /usr/lib64/libc.so.6 \
		/lib64/libm.so.6 /usr/lib64/libm.so.6 \
		/lib64/libmvec.so.1 /usr/lib64/libmvec.so.1 \
		/lib64/ld-linux-x86-64.so.2 /usr/lib64/ld-linux-x86-64.so.2
	do
		[ -e "$mkiso_crt" ] || [ -L "$mkiso_crt" ] || continue
		mkiso_copy_path "$mkiso_crt"
	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" || :
	done
	mkiso_copy_python_stdlib
	mkiso_install_meson
	mkiso_copy_cmake_data
	mkiso_copy_perl_lib
	mkiso_copy_path /usr/share/terminfo
	mkiso_copy_path /usr/share/zoneinfo
	mkiso_copy_path /etc/ssl
	mkiso_copy_path /etc/ca-certificates
	mkiso_copy_path /usr/share/ca-certificates
	mkiso_copy_path /etc/ld.so.conf
	mkiso_copy_path /etc/ld.so.conf.d
	mkiso_copy_path /usr/lib64/grub
	mkiso_copy_path /usr/lib/grub
	mkiso_copy_path /usr/share/grub
	if [ "$mkiso_with_firmware" -eq 1 ] && [ -d /lib/firmware ]; then
		printf '%s: copying /lib/firmware\n' "$sps_prog" >&2
		mkiso_copy_path /lib/firmware
	fi
	if [ "$mkiso_with_rust" -eq 1 ]; then
		mkiso_copy_path /usr/lib64/rustlib
		mkiso_copy_path /usr/bin/rustc
		mkiso_copy_path /usr/bin/cargo
		mkiso_copy_libs /usr/bin/rustc
		mkiso_copy_libs /usr/bin/cargo
	fi
	if [ "$mkiso_with_go" -eq 1 ]; then
		mkiso_copy_path /usr/lib64/go1.27.0
	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

mkiso_stamp_distro_branding

# 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
	mkiso_krel=$(uname -r)
	mkiso_modules=/lib/modules/$mkiso_krel
fi
if [ "$mkiso_copy_modules" -eq 1 ] && [ -d "$mkiso_modules" ]; then
	printf '%s: copying kernel modules from %s\n' \
		"$sps_prog" "$mkiso_modules" >&2
	mkdir -p "$mkiso_live/lib/modules"
	cp -a "$mkiso_modules" "$mkiso_live/lib/modules/"
fi

# PID 1 last, after seed, so a copied /sbin/init cannot stick around.
mkiso_install_pid1
mkiso_copy_runtime_libs
mkiso_close_libs
mkiso_force_busybox_sh
mkiso_ldconfig_live
mkiso_verify_live

if [ -n "$mkiso_layout" ] && [ -z "$mkiso_output" ]; then
	printf '%s\n' "$mkiso_live"
	exit 0
fi

# --- squashfs + initrd + ISO --------------------------------------------

command -v mksquashfs >/dev/null 2>&1 ||
	sps_die "$SPS_EX_PACKAGE" "mksquashfs is required"
command -v xorriso >/dev/null 2>&1 ||
	sps_die "$SPS_EX_PACKAGE" "xorriso is required"

if [ -z "$mkiso_kernel" ]; then
	if [ -f /boot/vmlinuz-$(uname -r) ]; then
		mkiso_kernel=/boot/vmlinuz-$(uname -r)
	elif [ -f /boot/vmlinuz ]; then
		mkiso_kernel=/boot/vmlinuz
	fi
fi
# Follow a single symlink so the ISO contains a real kernel, not a dangling link.
if [ -L "$mkiso_kernel" ]; then
	mkiso_ktarget=$(readlink "$mkiso_kernel")
	case $mkiso_ktarget in
		/*) mkiso_kernel=$mkiso_ktarget ;;
		*) mkiso_kernel=$(dirname "$mkiso_kernel")/$mkiso_ktarget ;;
	esac
fi
[ -f "$mkiso_kernel" ] ||
	sps_die "$SPS_EX_NOTFOUND" "kernel not found: $mkiso_kernel"

mkiso_iso=$mkiso_work/iso
mkdir -p "$mkiso_iso/live" "$mkiso_iso/boot/grub"
cp -a "$mkiso_kernel" "$mkiso_iso/live/vmlinuz"

printf '%s: writing squashfs (zstd)\n' "$sps_prog" >&2
# zstd mounts in seconds; xz is what made QEMU sit on loop0 while the
# 4G Plasma root decompressed. Level 3 is a live-ISO default: fast
# to build, fast to boot, still much smaller than gzip.
mkiso_squash_jobs=$(nproc 2>/dev/null || printf 2)
case $mkiso_squash_jobs in
	''|*[!0-9]*|0) mkiso_squash_jobs=2 ;;
esac
# -all-root: everything in a live root belongs to root. Without it an
# unprivileged build bakes the builder's uid into every file, and the booted
# system finds /usr owned by a user that does not exist on it. systemd
# refuses unit files it does not trust and sudo refuses to run at all.
if ! mksquashfs "$mkiso_live" "$mkiso_iso/live/filesystem.squashfs" \
	-comp zstd -Xcompression-level 3 -b 262144 -noappend -no-progress \
	-all-root -processors "$mkiso_squash_jobs" \
	-e boot tmp var/tmp var/cache
then
	printf '%s: zstd squashfs failed, trying xz\n' "$sps_prog" >&2
	if ! mksquashfs "$mkiso_live" "$mkiso_iso/live/filesystem.squashfs" \
		-comp xz -noappend -no-progress -all-root \
		-processors "$mkiso_squash_jobs" \
		-e boot tmp var/tmp var/cache
	then
		printf '%s: xz squashfs failed, trying gzip\n' "$sps_prog" >&2
		mksquashfs "$mkiso_live" "$mkiso_iso/live/filesystem.squashfs" \
			-comp gzip -noappend -no-progress -all-root \
			-processors "$mkiso_squash_jobs" \
			-e boot tmp var/tmp var/cache ||
			sps_die "$SPS_EX_BUILD" "mksquashfs failed"
	fi
fi

# Initramfs: busybox + init + filesystem modules.
mkiso_ird=$mkiso_work/initrd-root
rm -rf "$mkiso_ird"
mkdir -p "$mkiso_ird/bin" "$mkiso_ird/sbin" "$mkiso_ird/lib/modules" \
	"$mkiso_ird/proc" "$mkiso_ird/sys" "$mkiso_ird/dev" "$mkiso_ird/mnt"
cp -a "$mkiso_busybox" "$mkiso_ird/bin/busybox"
chmod 755 "$mkiso_ird/bin/busybox"
mkiso_busybox_links "$mkiso_ird/bin"
install -m 0755 "$mkiso_libdir/initrd-init" "$mkiso_ird/init"

if [ -d "$mkiso_modules" ]; then
	mkiso_kver=$(basename "$mkiso_modules")
	mkdir -p "$mkiso_ird/lib/modules/$mkiso_kver"
	# Copy only what the initramfs needs to find and mount the ISO.
	# Follow host module dependencies so overlay/loop are complete.
	mkiso_copy_one_mod()
	{
		mkiso_mp=$1
		[ -f "$mkiso_mp" ] || return 0
		mkiso_rel=${mkiso_mp#"$mkiso_modules"/}
		mkdir -p "$mkiso_ird/lib/modules/$mkiso_kver/$(dirname "$mkiso_rel")"
		case $mkiso_mp in
			*.ko.xz)
				xz -dc "$mkiso_mp" >"$mkiso_ird/lib/modules/$mkiso_kver/${mkiso_rel%.xz}"
				;;
			*.ko.zst)
				zstd -q -dc "$mkiso_mp" >"$mkiso_ird/lib/modules/$mkiso_kver/${mkiso_rel%.zst}"
				;;
			*)
				cp -a "$mkiso_mp" \
					"$mkiso_ird/lib/modules/$mkiso_kver/$mkiso_rel"
				;;
		esac
	}
	for mkiso_mod in squashfs overlay loop isofs cdrom sr_mod sd_mod \
		ahci nvme usb-storage xhci-pci ehci-hcd uhci-hcd \
		virtio_blk virtio_pci virtio_scsi virtio_net \
		nls_cp437 nls_iso8859-1 vfat fat crc32c libcrc32c \
		drm drm_kms_helper drm_shmem_helper drm_ttm_helper ttm \
		virtio_dma_buf virtio_gpu bochs simpledrm
	do
		if command -v modprobe >/dev/null 2>&1; then
			modprobe -D "$mkiso_mod" 2>/dev/null | while IFS= read -r mkiso_line
			do
				mkiso_cmd=${mkiso_line%% *}
				mkiso_mp=${mkiso_line#* }
				mkiso_mp=${mkiso_mp%% *}
				[ "$mkiso_cmd" = insmod ] || continue
				mkiso_copy_one_mod "$mkiso_mp"
			done
		fi
		find "$mkiso_modules" -name "${mkiso_mod}.ko*" 2>/dev/null |
			while IFS= read -r mkiso_mp; do
				mkiso_copy_one_mod "$mkiso_mp"
			done
	done
	cp -a "$mkiso_modules"/modules.dep \
		"$mkiso_ird/lib/modules/$mkiso_kver"/ 2>/dev/null || :
	cp -a "$mkiso_modules"/modules.alias \
		"$mkiso_ird/lib/modules/$mkiso_kver"/ 2>/dev/null || :
	cp -a "$mkiso_modules"/modules.builtin \
		"$mkiso_ird/lib/modules/$mkiso_kver"/ 2>/dev/null || :
fi

printf '%s: packing initramfs\n' "$sps_prog" >&2
# --owner root:root for the same reason as -all-root above: PID 1 runs out of
# this archive, and cpio would otherwise record the building user's uid.
mkiso_cpio_owner=--owner=root:root
printf '' | cpio -o -H newc --owner=root:root >/dev/null 2>&1 ||
	mkiso_cpio_owner=-R0:0
(
	cd "$mkiso_ird" || exit 1
	find . | cpio -o -H newc "$mkiso_cpio_owner" 2>/dev/null
) | gzip -n -6 >"$mkiso_iso/live/initrd.img" ||
	sps_die "$SPS_EX_BUILD" "initramfs cpio failed"

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 "$mkiso_title" {
	linux /live/vmlinuz boot=live sps.init=$mkiso_init console=tty0 console=ttyS0,115200n8
	initrd /live/initrd.img
}

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

# BIOS + UEFI hybrid ISO. grub-mkrescue on this host requires
# /usr/share/grub/unicode.pf2, which is not shipped, so mkiso builds
# the image with grub-mkimage and xorriso instead. It never runs
# grub-install on a disk.
command -v grub-mkimage >/dev/null 2>&1 ||
	sps_die "$SPS_EX_PACKAGE" "grub-mkimage is required"
mkiso_grub_pc_cmd=$(command -v grub-mkimage)
mkiso_grub_efi_cmd=$mkiso_grub_pc_cmd
mkiso_pc=
mkiso_efi=
for mkiso_gdir in /usr/lib64/grub /usr/lib/grub
do
	[ -d "$mkiso_gdir/i386-pc" ] && mkiso_pc=$mkiso_gdir/i386-pc
	[ -d "$mkiso_gdir/x86_64-efi" ] && mkiso_efi=$mkiso_gdir/x86_64-efi
done
if command -v brew >/dev/null 2>&1; then
	mkiso_brew_pc=$(brew --prefix i686-elf-grub 2>/dev/null || :)
	if [ -d "$mkiso_brew_pc/lib/i686-elf/grub/i386-pc" ]; then
		mkiso_pc=$mkiso_brew_pc/lib/i686-elf/grub/i386-pc
		if command -v i686-elf-grub-mkimage >/dev/null 2>&1; then
			mkiso_grub_pc_cmd=$(command -v i686-elf-grub-mkimage)
		fi
	fi
	mkiso_brew_efi=$(brew --prefix x86_64-elf-grub 2>/dev/null || :)
	if [ -d "$mkiso_brew_efi/lib/x86_64-elf/grub/x86_64-efi" ]; then
		mkiso_efi=$mkiso_brew_efi/lib/x86_64-elf/grub/x86_64-efi
		if command -v x86_64-elf-grub-mkimage >/dev/null 2>&1; then
			mkiso_grub_efi_cmd=$(command -v x86_64-elf-grub-mkimage)
		fi
	fi
fi
[ -n "$mkiso_pc" ] ||
	sps_die "$SPS_EX_PACKAGE" "GRUB i386-pc modules not found"

mkiso_grub_mods='iso9660 linux normal configfile echo search fat part_gpt part_msdos ext2 serial'
"$mkiso_grub_pc_cmd" -d "$mkiso_pc" -O i386-pc \
	-o "$mkiso_work/core.img" -p /boot/grub \
	biosdisk $mkiso_grub_mods ||
	sps_die "$SPS_EX_BUILD" "grub-mkimage (i386-pc) failed"
cat "$mkiso_pc/cdboot.img" "$mkiso_work/core.img" >"$mkiso_iso/boot/grub/bios.img"

mkdir -p "$mkiso_iso/EFI/BOOT"
if [ -n "$mkiso_efi" ]; then
	"$mkiso_grub_efi_cmd" -d "$mkiso_efi" -O x86_64-efi \
		-o "$mkiso_iso/EFI/BOOT/BOOTX64.EFI" \
		-p /boot/grub $mkiso_grub_mods ||
		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 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
	elif command -v mkfs.vfat >/dev/null 2>&1 || [ -x /sbin/mkfs.vfat ]; then
		mkiso_mkfs=$(command -v mkfs.vfat 2>/dev/null || printf /sbin/mkfs.vfat)
		"$mkiso_mkfs" "$mkiso_iso/boot/grub/efi.img" >/dev/null
		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
	fi
fi

printf '%s: writing ISO\n' "$sps_prog" >&2
case $mkiso_output in
	/*) ;;
	*) mkiso_output=$(CDPATH= cd -P "$(pwd)" && printf '%s/%s' "$(pwd)" "$mkiso_output") ;;
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 SPLUX_LIVE \
		-o "$mkiso_output" \
		-b boot/grub/bios.img -no-emul-boot -boot-load-size 4 \
		-boot-info-table --grub2-boot-info \
		--grub2-mbr "$mkiso_pc/boot_hybrid.img" \
		-append_partition 2 0xef "$mkiso_iso/boot/grub/efi.img" \
		-eltorito-alt-boot \
		-e --interval:appended_partition_2:all:: \
		-no-emul-boot \
		--mbr-force-bootable \
		"$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 SPLUX_LIVE \
		-o "$mkiso_output" \
		-b boot/grub/bios.img -no-emul-boot -boot-load-size 4 \
		-boot-info-table --grub2-boot-info \
		--grub2-mbr "$mkiso_pc/boot_hybrid.img" \
		--mbr-force-bootable \
		"$mkiso_iso" >"$mkiso_xorriso_log" 2>&1 ||
		sps_die "$SPS_EX_BUILD" "xorriso failed (see $mkiso_xorriso_log)"
fi

if [ -f "$mkiso_iso/boot/grub/efi.img" ] &&
	command -v fdisk >/dev/null 2>&1
then
	if ! fdisk -l "$mkiso_output" 2>/dev/null |
		grep -qE '(^|[[:space:]])(ef|EFI)'
	then
		sps_die "$SPS_EX_BUILD" \
			"generated ISO has no USB-visible EFI partition"
	fi
fi

printf '%s\n' "$mkiso_output"
ls -lh "$mkiso_output" >&2
exit 0