SPS · log · files · refs · tags · releases · people · languages · GitHub · commit
fd201b3
setup: add a dialog installer and mkiso live TTY images
Parents
Stat
MANIFESTO.md | 2 + Makefile | 23 +- README.md | 52 ++ bin/mkiso | 569 ++++++++++++++++ bin/setup | 1014 +++++++++++++++++++++++++++++ lib/mkiso/initrd-init | 75 +++ lib/mkiso/issue | 3 + lib/mkiso/live-init | 72 ++ lib/mkiso/motd | 20 + lib/mkiso/seed-bins | 127 ++++ lib/setup/extras | 72 ++ lib/setup/keymaps | 21 + lib/setup/locales | 32 + lib/setup/profiles/minimal.profile | 5 + lib/setup/profiles/plasma-desktop.profile | 14 + lib/setup/profiles/plasma-full.profile | 17 + lib/setup/profiles/server.profile | 9 + lib/setup/sets/bluetooth.set | 5 + lib/setup/sets/browsers.set | 5 + lib/setup/sets/containers.set | 7 + lib/setup/sets/devel-tools.set | 5 + lib/setup/sets/editors-extra.set | 7 + lib/setup/sets/firmware.set | 5 + lib/setup/sets/flatpak.set | 9 + lib/setup/sets/fonts-extra.set | 8 + lib/setup/sets/kde-apps.set | 5 + lib/setup/sets/languages.set | 5 + lib/setup/sets/multimedia.set | 13 + lib/setup/sets/network-extra.set | 12 + lib/setup/sets/nvidia.set | 7 + lib/setup/sets/office.set | 8 + lib/setup/sets/power.set | 8 + lib/setup/sets/printing.set | 7 + lib/setup/sets/qol-cli.set | 5 + lib/setup/sets/shells-extra.set | 6 + lib/setup/sets/ssh.set | 5 + lib/setup/sets/storage.set | 16 + lib/setup/sets/wifi.set | 7 + lib/setup/shells | 4 + lib/setup/timezones | 28 + man/mkiso.1 | 79 +++ man/setup.1 | 173 +++++ tests/setup-fixture/extras | 1 + tests/setup-fixture/keymaps | 2 + tests/setup-fixture/locales | 2 + tests/setup-fixture/profiles/tiny.profile | 4 + tests/setup-fixture/sets/extra.set | 5 + tests/setup-fixture/shells | 2 + tests/setup-fixture/timezones | 2 + tests/test_mkiso.sh | 52 ++ tests/test_setup.sh | 133 ++++ 51 files changed, 2768 insertions(+), 1 deletion(-)
Diff
diff --git a/MANIFESTO.md b/MANIFESTO.md
index 73a28c8..23e1cd7 100644
--- a/MANIFESTO.md
+++ b/MANIFESTO.md
@@ -34,6 +34,8 @@ pkdel low-level package removal
pkstat installed package queries
pkcheck installed-state verification
pkmark explicit/dependency package marking
+setup dialog-based system installer
+mkiso live TTY ISO builder
```
Each tool has a boundary.
diff --git a/Makefile b/Makefile
index 547f49b..58d79f1 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ MANDIR = $(PREFIX)/share/man
SYSCONFDIR = /etc/sps
DESTDIR =
-PROGRAMS = src sget mkpkg pkin pkdel pkstat pkcheck pkmark
+PROGRAMS = src sget mkpkg pkin pkdel pkstat pkcheck pkmark setup mkiso
AWK_LIBS = common.awk config.awk db.awk deps.awk index.awk package.awk recipe.awk repository.awk relations.awk version.awk
.PHONY: all check lint check-version source-archive release-check \
@@ -148,6 +148,27 @@ install:
@set -e; for page in man/*.5; do \
install -m 0644 $$page "$(DESTDIR)$(MANDIR)/man5/$${page##*/}"; \
done
+ install -d "$(DESTDIR)$(LIBDIR)/setup/profiles" \
+ "$(DESTDIR)$(LIBDIR)/setup/sets"
+ install -m 0644 lib/setup/keymaps lib/setup/timezones \
+ lib/setup/locales lib/setup/shells lib/setup/extras \
+ "$(DESTDIR)$(LIBDIR)/setup/"
+ @set -e; for f in lib/setup/profiles/*.profile; do \
+ install -m 0644 $$f "$(DESTDIR)$(LIBDIR)/setup/profiles/"; \
+ done
+ @set -e; for f in lib/setup/sets/*.set; do \
+ install -m 0644 $$f "$(DESTDIR)$(LIBDIR)/setup/sets/"; \
+ done
+ install -d "$(DESTDIR)$(LIBDIR)/mkiso"
+ @set -e; for f in lib/mkiso/*; do \
+ [ -f "$$f" ] || continue; \
+ case $$f in \
+ */live-init|*/initrd-init) \
+ install -m 0755 $$f "$(DESTDIR)$(LIBDIR)/mkiso/" ;; \
+ *) \
+ install -m 0644 $$f "$(DESTDIR)$(LIBDIR)/mkiso/" ;; \
+ esac; \
+ done
# Configuration is separate so installation never overwrites administrator
# policy. Existing files are deliberately left untouched.
diff --git a/README.md b/README.md
index b94517b..a07c721 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,8 @@ SPS is a set of tools rather than one large front end:
- `pkstat` reads the installed package database.
- `pkcheck` checks installed files and database consistency.
- `pkmark` changes whether an installed package is explicit or dependency-only.
+- `setup` is a dialog(1) system installer (Slackware/FreeBSD style).
+- `mkiso` builds a live TTY ISO you can flash and boot into `setup`.
The split matters. `pkin` does not search repositories. `pkdel` does not solve
dependencies. `src` does not install anything. `sget` ties the lower-level tools
@@ -136,6 +138,56 @@ pkdel foo
This is useful for recovery work, distro installers, package development, and
cases where the administrator wants to bypass dependency policy on purpose.
+## Install a system
+
+`setup` is the distro installer. It uses `dialog` screens, or the same choices
+from flags for scripts:
+
+```sh
+setup
+setup --plan --profile plasma-desktop --target /mnt --user alex
+setup --non-interactive --profile minimal --target /mnt --hostname darkstar
+```
+
+Profiles are `minimal`, `server`, `plasma-desktop`, and `plasma-full`. Optional
+sets cover CLI tools, development, languages, KDE apps, NVIDIA, Flatpak, power
+management, multimedia, printing, browsers, extra shells, fonts, office tools,
+SSH, and more. `setup --list-sets` prints them. There is also an extra-package
+checklist (`--extra`) plus locale and login-shell screens.
+
+The installer writes hostname, timezone, keymap, and a first user into the
+target, then runs `src update` and `sget install`. It does not partition disks
+and does not run `grub-install` unless you pass `--install-bootloader`, which
+still refuses to guess a disk.
+
+## Live TTY ISO
+
+`mkiso` writes a hybrid ISO. Flash it to USB, boot to a console, mount a
+destination filesystem on `/mnt`, and run `setup`. A GitHub release attached
+to this repository is the downloadable image:
+
+```sh
+mkiso --output dist/sps-live.iso \
+ --busybox /path/to/busybox \
+ --core /usr/src/sps/core \
+ --extra /usr/src/sps/extra
+```
+
+`--layout DIR` writes the live root without making an ISO. `--no-seed` skips
+copying the host compiler. `--with-firmware` copies `/lib/firmware` (large;
+published images omit it). The live image never runs `grub-install` on a disk.
+
+Download a ready-made hybrid ISO from this repository's GitHub Releases, flash
+it to USB, boot the "SPS live TTY" entry, mount a destination filesystem on
+`/mnt`, and run `setup`. The image includes the installer, sps-core and
+sps-extra recipes, and a host seed compiler so `setup` can build packages.
+A full Plasma install compiles Qt and KDE from source and takes a long time.
+
+```sh
+# flash (example)
+dd if=sps-live-tty.iso of=/dev/sdX bs=4M status=progress conv=fsync
+```
+
## Alternate roots
`SPS_ROOT` points SPS at another filesystem tree. Database, cache, and build
diff --git a/bin/mkiso b/bin/mkiso
new file mode 100755
index 0000000..aa0f479
--- /dev/null
+++ b/bin/mkiso
@@ -0,0 +1,569 @@
+#!/bin/sh
+# Build a bootable SPS live TTY ISO. Users flash it, boot to a console, mount
+# a destination filesystem, and run setup(1). It never runs grub-install on a
+# disk.
+
+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 TTY ISO (or a layout directory) that boots to a console with
+setup(1) on PATH.
+
+options:
+ --output FILE write a hybrid ISO
+ --layout DIR populate a live root and stop (tests, inspection)
+ --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
+ --with-firmware copy /lib/firmware (large; omit from published ISOs)
+ --no-modules do not copy /lib/modules (smaller, less hardware)
+ --no-seed do not copy host compiler tools (layout/tests)
+ -h, --help
+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_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 ;;
+ -*)
+ 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
+
+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 -------------------------------------------------------
+
+mkiso_copy_path()
+{
+ mkiso_src=$1
+ [ -e "$mkiso_src" ] || [ -L "$mkiso_src" ] || return 0
+ mkiso_dest=$mkiso_live$mkiso_src
+ mkdir -p "$(dirname "$mkiso_dest")" || return 1
+ if [ -d "$mkiso_src" ] && [ ! -L "$mkiso_src" ]; then
+ mkdir -p "$mkiso_dest"
+ cp -a "$mkiso_src" "$mkiso_dest" 2>/dev/null ||
+ cp -R "$mkiso_src" "$mkiso_dest" 2>/dev/null || :
+ else
+ cp -a "$mkiso_src" "$mkiso_dest" 2>/dev/null ||
+ cp "$mkiso_src" "$mkiso_dest" 2>/dev/null || :
+ fi
+}
+
+mkiso_copy_libs()
+{
+ mkiso_bin=$1
+ [ -f "$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*) 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
+ done
+}
+
+# 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
+ ln -sf busybox "$mkiso_bbdir/$mkiso_ap"
+ done
+ ln -sf busybox "$mkiso_bbdir/sh"
+}
+
+# --- 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/bin" "$mkiso_live/sbin" "$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"
+
+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"
+ ln -sf ../bin/busybox "$mkiso_live/sbin/busybox" 2>/dev/null || :
+fi
+
+install -m 0755 "$mkiso_libdir/live-init" "$mkiso_live/sbin/init"
+install -m 0644 "$mkiso_libdir/motd" "$mkiso_live/etc/motd"
+install -m 0644 "$mkiso_libdir/issue" "$mkiso_live/etc/issue"
+printf '%s\n' live >"$mkiso_live/etc/sps/live"
+printf '%s\n' sps-live >"$mkiso_live/etc/hostname"
+printf '%s\n' 'root:x:0:0:root:/root:/bin/sh' >"$mkiso_live/etc/passwd"
+printf '%s\n' 'root:x:0:' >"$mkiso_live/etc/group"
+# Empty password on the live console only.
+printf '%s\n' 'root::0:0:99999:7:::' >"$mkiso_live/etc/shadow"
+chmod 600 "$mkiso_live/etc/shadow"
+
+printf '%s\n' \
+ 'db /var/lib/sps' \
+ 'cache /var/cache/sps' \
+ 'build /var/tmp/sps' \
+ 'repo_root /usr/src/sps' \
+ "makejobs ${SPS_MAKEJOBS:-$(nproc 2>/dev/null || printf 2)}" \
+ 'compression auto' \
+ 'preserve etc' \
+ >"$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
+
+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/include
+ mkiso_copy_path /usr/lib64/python3.12
+ 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
+
+# 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
+
+# Busybox getty/init must win over a dangling seed copy of /sbin/init.
+install -m 0755 "$mkiso_libdir/live-init" "$mkiso_live/sbin/init"
+
+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 (this takes a while)\n' "$sps_prog" >&2
+mksquashfs "$mkiso_live" "$mkiso_iso/live/filesystem.squashfs" \
+ -comp xz -e boot -noappend >/dev/null ||
+ sps_die "$SPS_EX_BUILD" "mksquashfs failed"
+
+# 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
+ 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
+(
+ cd "$mkiso_ird" || exit 1
+ find . | cpio -o -H newc 2>/dev/null
+) | gzip -n -9 >"$mkiso_iso/live/initrd.img" ||
+ sps_die "$SPS_EX_BUILD" "initramfs cpio failed"
+
+cat >"$mkiso_iso/boot/grub/grub.cfg" <<'EOF'
+set timeout=8
+set default=0
+serial --unit=0 --speed=115200
+terminal_input serial console
+terminal_output serial console
+
+menuentry "SPS live TTY" {
+ linux /live/vmlinuz boot=live console=tty0 console=ttyS0,115200n8
+ initrd /live/initrd.img
+}
+
+menuentry "SPS live TTY (run setup)" {
+ linux /live/vmlinuz boot=live 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_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
+[ -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'
+grub-mkimage -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
+ grub-mkimage -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 SPS_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 SPS_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" \
+ -eltorito-alt-boot -e boot/grub/efi.img -no-emul-boot \
+ -isohybrid-gpt-basdat \
+ "$mkiso_iso" >"$mkiso_xorriso_log" 2>&1 ||
+ sps_die "$SPS_EX_BUILD" "xorriso failed (see $mkiso_xorriso_log)"
+else
+ xorriso -as mkisofs -r -J -V SPS_LIVE \
+ -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" \
+ "$mkiso_iso" >"$mkiso_xorriso_log" 2>&1 ||
+ sps_die "$SPS_EX_BUILD" "xorriso failed (see $mkiso_xorriso_log)"
+fi
+
+printf '%s\n' "$mkiso_output"
+ls -lh "$mkiso_output" >&2
+exit 0
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 0000000..e77ba47
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,1014 @@
+#!/bin/sh
+# SPS system installer. Interactive screens use dialog(1); every choice is
+# also available as a command-line option for scripts and tests.
+
+sps_prog=setup
+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' "setup: cannot load $SPS_LIBDIR/common.sh" >&2
+ exit 1
+fi
+sps_load_config || exit $?
+
+setup_usage()
+{
+ cat <<'EOF'
+usage: setup [options]
+
+Install an SPS system into a target root using dialog(1) screens, or the
+same choices from the command line.
+
+options:
+ --target DIR installation root (SPS_ROOT)
+ --profile NAME minimal, server, plasma-desktop, or plasma-full
+ --enable SET[,SET...] extra sets (repeatable)
+ --disable SET[,SET...] extra sets to leave off (repeatable)
+ --hostname NAME
+ --timezone ZONE e.g. UTC or America/Los_Angeles
+ --keymap MAP e.g. us, uk, de, dvorak
+ --locale NAME e.g. C.UTF-8 or en_US.UTF-8
+ --shell PATH login shell for the first user
+ --user NAME first unprivileged user (uid 1000)
+ --user-gecos TEXT
+ --extra PKG[,PKG...] individual packages in addition to sets
+ --ssh yes|no include the ssh set (OpenSSH)
+ --printing yes|no include the printing set (CUPS)
+ --flatpak yes|no add Flathub after install when flatpak is present
+ --nvidia yes|no include the nvidia set
+ --bootloader none|grub write bootloader notes; never runs grub-install
+ --install-bootloader actually run grub-install (explicit, dangerous)
+ --update|--no-update run or skip src update before installing
+ --non-interactive do not start dialog; all choices from options
+ --plan print the package list and exit
+ --write-answers FILE write the resolved answers
+ --read-answers FILE load answers (options override file values)
+ --list-profiles
+ --list-sets
+ -h, --help
+EOF
+}
+
+setup_dir=${SPS_SETUP_DIR:-$SPS_LIBDIR/setup}
+setup_work=
+setup_interactive=1
+setup_plan=0
+setup_list_profiles=0
+setup_list_sets=0
+setup_do_update=1
+setup_install_bootloader=0
+setup_target=
+setup_profile=
+setup_hostname=
+setup_timezone=
+setup_keymap=
+setup_locale=
+setup_shell=
+setup_user=
+setup_user_gecos=
+setup_extra=
+setup_ssh=
+setup_printing=
+setup_flatpak=
+setup_nvidia=
+setup_bootloader=none
+setup_answers_in=
+setup_answers_out=
+setup_enable=
+setup_disable=
+
+setup_cleanup()
+{
+ if [ -n "$setup_work" ] && [ -d "$setup_work" ]; then
+ rm -rf "$setup_work"
+ fi
+}
+
+setup_fail()
+{
+ setup_code=$1
+ shift
+ printf '%s: %s\n' "$sps_prog" "$*" >&2
+ exit "$setup_code"
+}
+
+setup_need_value()
+{
+ [ "$#" -ge 2 ] && [ -n "${2-}" ] ||
+ setup_fail "$SPS_EX_USAGE" "option '$1' requires an argument"
+}
+
+setup_append_csv()
+{
+ setup_dest=$1
+ setup_csv=$2
+ setup_old=$3
+ setup_item=
+ setup_rest=$setup_csv,
+ while [ -n "$setup_rest" ]; do
+ setup_item=${setup_rest%%,*}
+ setup_rest=${setup_rest#*,}
+ [ -n "$setup_item" ] || continue
+ case " $setup_old " in
+ *" $setup_item "*) ;;
+ *) setup_old="$setup_old $setup_item" ;;
+ esac
+ done
+ eval "$setup_dest=\"\$(printf '%s' \"\$setup_old\" | sed 's/^ //')\""
+}
+
+while [ "$#" -gt 0 ]; do
+ case $1 in
+ -h|--help)
+ setup_usage
+ exit 0
+ ;;
+ --target)
+ setup_need_value "$@"
+ setup_target=$2
+ shift 2
+ ;;
+ --profile)
+ setup_need_value "$@"
+ setup_profile=$2
+ shift 2
+ ;;
+ --enable)
+ setup_need_value "$@"
+ setup_append_csv setup_enable "$2" "$setup_enable"
+ shift 2
+ ;;
+ --disable)
+ setup_need_value "$@"
+ setup_append_csv setup_disable "$2" "$setup_disable"
+ shift 2
+ ;;
+ --hostname)
+ setup_need_value "$@"
+ setup_hostname=$2
+ shift 2
+ ;;
+ --timezone)
+ setup_need_value "$@"
+ setup_timezone=$2
+ shift 2
+ ;;
+ --keymap)
+ setup_need_value "$@"
+ setup_keymap=$2
+ shift 2
+ ;;
+ --locale)
+ setup_need_value "$@"
+ setup_locale=$2
+ shift 2
+ ;;
+ --shell)
+ setup_need_value "$@"
+ setup_shell=$2
+ shift 2
+ ;;
+ --extra)
+ setup_need_value "$@"
+ setup_append_csv setup_extra "$2" "$setup_extra"
+ shift 2
+ ;;
+ --ssh)
+ setup_need_value "$@"
+ setup_ssh=$2
+ shift 2
+ ;;
+ --printing)
+ setup_need_value "$@"
+ setup_printing=$2
+ shift 2
+ ;;
+ --user)
+ setup_need_value "$@"
+ setup_user=$2
+ shift 2
+ ;;
+ --user-gecos)
+ setup_need_value "$@"
+ setup_user_gecos=$2
+ shift 2
+ ;;
+ --flatpak)
+ setup_need_value "$@"
+ setup_flatpak=$2
+ shift 2
+ ;;
+ --nvidia)
+ setup_need_value "$@"
+ setup_nvidia=$2
+ shift 2
+ ;;
+ --bootloader)
+ setup_need_value "$@"
+ setup_bootloader=$2
+ shift 2
+ ;;
+ --install-bootloader)
+ setup_install_bootloader=1
+ shift
+ ;;
+ --update)
+ setup_do_update=1
+ shift
+ ;;
+ --no-update)
+ setup_do_update=0
+ shift
+ ;;
+ --non-interactive)
+ setup_interactive=0
+ shift
+ ;;
+ --plan)
+ setup_plan=1
+ setup_interactive=0
+ shift
+ ;;
+ --write-answers)
+ setup_need_value "$@"
+ setup_answers_out=$2
+ shift 2
+ ;;
+ --read-answers)
+ setup_need_value "$@"
+ setup_answers_in=$2
+ shift 2
+ ;;
+ --list-profiles)
+ setup_list_profiles=1
+ shift
+ ;;
+ --list-sets)
+ setup_list_sets=1
+ shift
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ setup_fail "$SPS_EX_USAGE" "unknown option: $1"
+ ;;
+ *)
+ setup_fail "$SPS_EX_USAGE" "unexpected argument: $1"
+ ;;
+ esac
+done
+
+[ "$#" -eq 0 ] || setup_fail "$SPS_EX_USAGE" "unexpected argument: $1"
+
+setup_field()
+{
+ setup_file=$1
+ setup_key=$2
+ awk -v k="$setup_key" '
+ $1 == k {
+ $1 = ""
+ sub(/^[ \t]+/, "")
+ print
+ exit
+ }
+ ' "$setup_file"
+}
+
+setup_packages_from()
+{
+ awk '
+ $1 == "package" {
+ $1 = ""
+ sub(/^[ \t]+/, "")
+ if ($0 != "") print
+ }
+ ' "$1"
+}
+
+setup_profile_file()
+{
+ printf '%s/profiles/%s.profile' "$setup_dir" "$1"
+}
+
+setup_set_file()
+{
+ printf '%s/sets/%s.set' "$setup_dir" "$1"
+}
+
+setup_each_profile()
+{
+ setup_p=
+ for setup_p in "$setup_dir"/profiles/*.profile; do
+ [ -f "$setup_p" ] || continue
+ setup_field "$setup_p" name
+ done
+}
+
+setup_each_set()
+{
+ setup_s=
+ for setup_s in "$setup_dir"/sets/*.set; do
+ [ -f "$setup_s" ] || continue
+ setup_field "$setup_s" name
+ done
+}
+
+setup_valid_profile()
+{
+ [ -f "$(setup_profile_file "$1")" ]
+}
+
+setup_valid_set()
+{
+ [ -f "$(setup_set_file "$1")" ]
+}
+
+setup_default_sets_for_profile()
+{
+ setup_ds=
+ setup_sf=
+ for setup_sf in "$setup_dir"/sets/*.set; do
+ [ -f "$setup_sf" ] || continue
+ setup_sn=$(setup_field "$setup_sf" name)
+ setup_def=$(setup_field "$setup_sf" default)
+ [ "$setup_def" = 1 ] || continue
+ case $setup_profile in
+ plasma-desktop|plasma-full)
+ setup_ds="$setup_ds $setup_sn"
+ ;;
+ *)
+ # Power defaults on for desktops only.
+ [ "$setup_sn" = power ] && continue
+ [ "$setup_sn" = qol-cli ] &&
+ setup_ds="$setup_ds $setup_sn"
+ ;;
+ esac
+ done
+ printf '%s' "$setup_ds" | sed 's/^ //'
+}
+
+if [ "$setup_list_profiles" -eq 1 ]; then
+ for setup_pf in "$setup_dir"/profiles/*.profile; do
+ [ -f "$setup_pf" ] || continue
+ printf '%s\t%s\t%s\n' \
+ "$(setup_field "$setup_pf" name)" \
+ "$(setup_field "$setup_pf" title)" \
+ "$(setup_field "$setup_pf" description)"
+ done
+ exit 0
+fi
+
+if [ "$setup_list_sets" -eq 1 ]; then
+ for setup_sf in "$setup_dir"/sets/*.set; do
+ [ -f "$setup_sf" ] || continue
+ printf '%s\t%s\t%s\n' \
+ "$(setup_field "$setup_sf" name)" \
+ "$(setup_field "$setup_sf" title)" \
+ "$(setup_field "$setup_sf" description)"
+ done
+ exit 0
+fi
+
+setup_load_answers()
+{
+ setup_af=$1
+ [ -f "$setup_af" ] && [ -r "$setup_af" ] ||
+ setup_fail "$SPS_EX_NOTFOUND" "cannot read answers file: $setup_af"
+ while IFS= read -r setup_line || [ -n "$setup_line" ]; do
+ case $setup_line in
+ ''|\#*) continue ;;
+ esac
+ setup_ak=${setup_line%% *}
+ setup_av=${setup_line#* }
+ [ "$setup_ak" = "$setup_line" ] && setup_av=
+ case $setup_ak in
+ target) [ -n "$setup_target" ] || setup_target=$setup_av ;;
+ profile) [ -n "$setup_profile" ] || setup_profile=$setup_av ;;
+ hostname) [ -n "$setup_hostname" ] || setup_hostname=$setup_av ;;
+ timezone) [ -n "$setup_timezone" ] || setup_timezone=$setup_av ;;
+ keymap) [ -n "$setup_keymap" ] || setup_keymap=$setup_av ;;
+ locale) [ -n "$setup_locale" ] || setup_locale=$setup_av ;;
+ shell) [ -n "$setup_shell" ] || setup_shell=$setup_av ;;
+ user) [ -n "$setup_user" ] || setup_user=$setup_av ;;
+ user-gecos) [ -n "$setup_user_gecos" ] || setup_user_gecos=$setup_av ;;
+ extra) setup_append_csv setup_extra "$setup_av" "$setup_extra" ;;
+ ssh) [ -n "$setup_ssh" ] || setup_ssh=$setup_av ;;
+ printing) [ -n "$setup_printing" ] || setup_printing=$setup_av ;;
+ flatpak) [ -n "$setup_flatpak" ] || setup_flatpak=$setup_av ;;
+ nvidia) [ -n "$setup_nvidia" ] || setup_nvidia=$setup_av ;;
+ bootloader) [ "$setup_bootloader" = none ] && [ -n "$setup_av" ] && setup_bootloader=$setup_av ;;
+ enable) setup_append_csv setup_enable "$setup_av" "$setup_enable" ;;
+ disable) setup_append_csv setup_disable "$setup_av" "$setup_disable" ;;
+ update)
+ [ "$setup_av" = no ] && setup_do_update=0
+ [ "$setup_av" = yes ] && setup_do_update=1
+ ;;
+ esac
+ done <"$setup_af"
+}
+
+[ -z "$setup_answers_in" ] || setup_load_answers "$setup_answers_in"
+
+setup_yesno_norm()
+{
+ case $1 in
+ yes|y|true|1) printf 'yes\n' ;;
+ no|n|false|0|'') printf 'no\n' ;;
+ *) setup_fail "$SPS_EX_USAGE" "expected yes or no, got '$1'" ;;
+ esac
+}
+
+setup_dialog()
+{
+ command -v dialog >/dev/null 2>&1 ||
+ setup_fail "$SPS_EX_PACKAGE" \
+ "dialog is not on PATH; install it or use --non-interactive"
+ dialog --backtitle "SPS Linux installer" --output-fd 1 "$@"
+}
+
+setup_msg()
+{
+ if [ "$setup_interactive" -eq 1 ]; then
+ dialog --backtitle "SPS Linux installer" --msgbox "$1" 12 64 || :
+ else
+ printf '%s\n' "$1"
+ fi
+}
+
+setup_confirm()
+{
+ if [ "$setup_interactive" -eq 1 ]; then
+ dialog --backtitle "SPS Linux installer" --yesno "$1" 14 64
+ else
+ return 0
+ fi
+}
+
+# Interactive defaults when flags were omitted.
+if [ "$setup_interactive" -eq 1 ] && [ ! -t 0 ]; then
+ setup_fail "$SPS_EX_USAGE" \
+ "stdin is not a terminal; use --non-interactive"
+fi
+
+if [ "$setup_interactive" -eq 1 ]; then
+ command -v dialog >/dev/null 2>&1 ||
+ setup_fail "$SPS_EX_PACKAGE" \
+ "dialog is required for interactive setup"
+fi
+
+setup_work=$(sps_mktemp_dir "${TMPDIR:-/tmp}" setup) || exit $?
+trap setup_cleanup 0 HUP INT TERM
+
+if [ "$setup_interactive" -eq 1 ]; then
+ if [ -f /etc/sps/live ]; then
+ setup_msg \
+ "This is the SPS live TTY. Mount the destination filesystem on /mnt first.
+
+Then continue. The installer never partitions disks. Bootloader install is a separate, explicit step."
+ else
+ setup_msg \
+ "This installer configures an SPS root the way Slackware and FreeBSD setup do: a sequence of dialog screens, then a visible package install.
+
+It never partitions disks. Point it at a mounted filesystem. Installing the bootloader is a separate, explicit step."
+ fi
+
+ if [ -z "$setup_target" ]; then
+ setup_target=$(setup_dialog --title "Installation target" \
+ --inputbox "Absolute path of the target root.
+
+Use /mnt after mounting the destination filesystem. / is the live system." \
+ 10 64 "${setup_target:-/mnt}") ||
+ setup_fail "$SPS_EX_USAGE" "cancelled"
+ fi
+
+ if [ -z "$setup_profile" ]; then
+ setup_menu_args=
+ for setup_pf in "$setup_dir"/profiles/*.profile; do
+ [ -f "$setup_pf" ] || continue
+ setup_pn=$(setup_field "$setup_pf" name)
+ setup_pt=$(setup_field "$setup_pf" title)
+ setup_menu_args="$setup_menu_args $setup_pn '$setup_pt'"
+ done
+ setup_profile=$(eval "setup_dialog --title 'Installation profile' \
+ --menu 'Choose a base system.' 16 64 8 $setup_menu_args") ||
+ setup_fail "$SPS_EX_USAGE" "cancelled"
+ fi
+
+ if [ -z "$setup_hostname" ]; then
+ setup_hostname=$(setup_dialog --title "Hostname" \
+ --inputbox "System hostname." 8 50 "${setup_hostname:-sps}") ||
+ setup_fail "$SPS_EX_USAGE" "cancelled"
+ fi
+
+ if [ -z "$setup_timezone" ]; then
+ setup_tz_args=
+ while IFS= read -r setup_tz || [ -n "$setup_tz" ]; do
+ [ -n "$setup_tz" ] || continue
+ setup_tz_args="$setup_tz_args $setup_tz '$setup_tz'"
+ done <"$setup_dir/timezones"
+ setup_timezone=$(eval "setup_dialog --title 'Timezone' \
+ --menu 'Select a timezone.' 20 60 12 $setup_tz_args") ||
+ setup_fail "$SPS_EX_USAGE" "cancelled"
+ fi
+
+ if [ -z "$setup_locale" ]; then
+ setup_loc_args=
+ while IFS= read -r setup_loc || [ -n "$setup_loc" ]; do
+ [ -n "$setup_loc" ] || continue
+ setup_loc_args="$setup_loc_args $setup_loc '$setup_loc'"
+ done <"$setup_dir/locales"
+ setup_locale=$(eval "setup_dialog --title 'Locale' \
+ --menu 'LANG for /etc/locale.conf. C.UTF-8 needs no generated locales.' 20 60 12 $setup_loc_args") ||
+ setup_fail "$SPS_EX_USAGE" "cancelled"
+ fi
+
+ if [ -z "$setup_keymap" ]; then
+ setup_km_args=
+ while IFS= read -r setup_km || [ -n "$setup_km" ]; do
+ [ -n "$setup_km" ] || continue
+ setup_km_args="$setup_km_args $setup_km '$setup_km'"
+ done <"$setup_dir/keymaps"
+ setup_keymap=$(eval "setup_dialog --title 'Keyboard' \
+ --menu 'Keyboard map for the console.' 18 50 12 $setup_km_args") ||
+ setup_fail "$SPS_EX_USAGE" "cancelled"
+ fi
+
+ setup_chk_args=
+ for setup_sf in "$setup_dir"/sets/*.set; do
+ [ -f "$setup_sf" ] || continue
+ setup_sn=$(setup_field "$setup_sf" name)
+ setup_st=$(setup_field "$setup_sf" title)
+ setup_sd=$(setup_field "$setup_sf" description)
+ setup_on=off
+ case " $setup_enable " in *" $setup_sn "*) setup_on=on ;; esac
+ if [ -z "$setup_enable" ]; then
+ setup_def=$(setup_field "$setup_sf" default)
+ if [ "$setup_def" = 1 ]; then
+ case $setup_profile in
+ plasma-desktop|plasma-full) setup_on=on ;;
+ *)
+ [ "$setup_sn" = qol-cli ] && setup_on=on
+ ;;
+ esac
+ fi
+ fi
+ case " $setup_disable " in *" $setup_sn "*) setup_on=off ;; esac
+ setup_chk_args="$setup_chk_args $setup_sn '$setup_st' $setup_on"
+ done
+ setup_sel=$(eval "setup_dialog --title 'Optional sets' \
+ --checklist 'Space toggles a set. These are installed in addition to the profile.' \
+ 20 72 12 $setup_chk_args") ||
+ setup_fail "$SPS_EX_USAGE" "cancelled"
+ setup_enable=
+ # dialog may quote tags
+ set -- $setup_sel
+ for setup_tok; do
+ setup_tok=${setup_tok#\"}
+ setup_tok=${setup_tok%\"}
+ [ -n "$setup_tok" ] || continue
+ setup_append_csv setup_enable "$setup_tok" "$setup_enable"
+ done
+
+ if [ -f "$setup_dir/extras" ]; then
+ setup_ex_args=
+ while IFS= read -r setup_exline || [ -n "$setup_exline" ]; do
+ [ -n "$setup_exline" ] || continue
+ case $setup_exline in \#*) continue ;; esac
+ setup_en=${setup_exline%% *}
+ setup_et=${setup_exline#* }
+ [ -n "$setup_en" ] || continue
+ [ "$setup_et" != "$setup_exline" ] || setup_et=$setup_en
+ 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"
+ done <"$setup_dir/extras"
+ if [ -n "$setup_ex_args" ]; then
+ setup_sel=$(eval "setup_dialog --title 'Extra packages' \
+ --checklist 'Optional packages on top of the profile and sets. Space toggles an item.' \
+ 22 72 14 $setup_ex_args") ||
+ setup_fail "$SPS_EX_USAGE" "cancelled"
+ set -- $setup_sel
+ for setup_tok; do
+ setup_tok=${setup_tok#\"}
+ setup_tok=${setup_tok%\"}
+ [ -n "$setup_tok" ] || continue
+ setup_append_csv setup_extra "$setup_tok" "$setup_extra"
+ done
+ fi
+ fi
+
+ if [ -z "$setup_nvidia" ]; then
+ if setup_dialog --title "NVIDIA" --yesno \
+ "Include NVIDIA open kernel modules and user-space utilities?
+
+This requires a matching kernel on the target and is off by default." 10 64
+ then
+ setup_nvidia=yes
+ else
+ setup_nvidia=no
+ fi
+ fi
+
+ if [ -z "$setup_flatpak" ]; then
+ if setup_dialog --title "Flatpak" --yesno \
+ "Install Flatpak and add the Flathub remote after package installation?" 9 64
+ then
+ setup_flatpak=yes
+ else
+ setup_flatpak=no
+ fi
+ fi
+
+ if [ -z "$setup_user" ]; then
+ setup_user=$(setup_dialog --title "First user" \
+ --inputbox "Unprivileged account (uid 1000). Leave empty to skip." \
+ 9 50 "${setup_user:-}") ||
+ setup_fail "$SPS_EX_USAGE" "cancelled"
+ fi
+
+ if [ -z "$setup_user_gecos" ] && [ -n "$setup_user" ]; then
+ setup_user_gecos=$(setup_dialog --title "User full name" \
+ --inputbox "GECOS / display name for $setup_user." \
+ 8 50 "$setup_user") ||
+ setup_fail "$SPS_EX_USAGE" "cancelled"
+ fi
+
+ if [ -z "$setup_shell" ]; then
+ setup_sh_args=
+ if [ -f "$setup_dir/shells" ]; then
+ while IFS= read -r setup_sh || [ -n "$setup_sh" ]; do
+ [ -n "$setup_sh" ] || continue
+ setup_sh_args="$setup_sh_args $setup_sh '$setup_sh'"
+ done <"$setup_dir/shells"
+ fi
+ [ -n "$setup_sh_args" ] || setup_sh_args="/bin/sh '/bin/sh' /bin/bash '/bin/bash'"
+ setup_shell=$(eval "setup_dialog --title 'Login shell' \
+ --menu 'Login shell for the first user, or the default account later. zsh and fish need the shells-extra set.' 16 64 6 $setup_sh_args") ||
+ setup_fail "$SPS_EX_USAGE" "cancelled"
+ fi
+
+ setup_dialog --title "Network" --msgbox \
+ "This installer does not configure DHCP, static addresses, or wireless.
+
+After the first boot, put an interface file in /etc/network or use the NetworkManager applet on Plasma. Notes are written to /root/sps-network.txt." 12 64 || :
+
+ setup_dialog --title "Disks" --msgbox \
+ "This installer does not partition, format, or write /etc/fstab.
+
+Mount the destination filesystem on the target path before running setup. Swap and fstab remain administrator work. Notes are written to /root/sps-fstab.txt." 12 64 || :
+
+ setup_bl=$(setup_dialog --title "Bootloader" \
+ --menu "SPS never runs grub-install unless you pass --install-bootloader. The usual choice is to write notes only." \
+ 14 64 3 \
+ none "Write /root/sps-bootloader.txt only" \
+ grub "Write GRUB notes (still does not install)") ||
+ setup_fail "$SPS_EX_USAGE" "cancelled"
+ setup_bootloader=$setup_bl
+fi
+
+# Non-interactive defaults
+[ -n "$setup_target" ] || setup_target=/mnt
+[ -n "$setup_profile" ] || setup_profile=minimal
+[ -n "$setup_hostname" ] || setup_hostname=sps
+[ -n "$setup_timezone" ] || setup_timezone=UTC
+[ -n "$setup_keymap" ] || setup_keymap=us
+[ -n "$setup_locale" ] || setup_locale=C.UTF-8
+[ -n "$setup_shell" ] || setup_shell=/bin/sh
+setup_flatpak=$(setup_yesno_norm "${setup_flatpak:-no}")
+setup_nvidia=$(setup_yesno_norm "${setup_nvidia:-no}")
+setup_ssh=$(setup_yesno_norm "${setup_ssh:-no}")
+setup_printing=$(setup_yesno_norm "${setup_printing:-no}")
+case $setup_bootloader in
+ none|grub) ;;
+ *) setup_fail "$SPS_EX_USAGE" "bootloader must be none or grub" ;;
+esac
+
+setup_valid_profile "$setup_profile" ||
+ setup_fail "$SPS_EX_NOTFOUND" "unknown profile: $setup_profile"
+
+case $setup_target in
+ /*) ;;
+ *) setup_fail "$SPS_EX_USAGE" "target must be an absolute path: $setup_target" ;;
+esac
+
+case $setup_hostname in
+ *[!A-Za-z0-9.-]*|'') setup_fail "$SPS_EX_USAGE" "invalid hostname: $setup_hostname" ;;
+esac
+
+case $setup_locale in
+ *[!A-Za-z0-9._+-]*|'') setup_fail "$SPS_EX_USAGE" "invalid locale: $setup_locale" ;;
+esac
+
+case $setup_shell in
+ /*) ;;
+ *) setup_fail "$SPS_EX_USAGE" "shell must be an absolute path: $setup_shell" ;;
+esac
+
+if [ -n "$setup_user" ]; then
+ case $setup_user in
+ *[!a-z0-9_-]*|''|root|nobody)
+ setup_fail "$SPS_EX_USAGE" "invalid user name: $setup_user"
+ ;;
+ esac
+fi
+
+# Non-interactive installs start from profile defaults, then --enable.
+if [ "$setup_interactive" -eq 0 ]; then
+ setup_defs=$(setup_default_sets_for_profile)
+ for setup_def in $setup_defs; do
+ setup_append_csv setup_enable "$setup_def" "$setup_enable"
+ done
+fi
+
+# Apply nvidia/flatpak convenience flags to sets.
+if [ "$setup_nvidia" = yes ]; then
+ setup_append_csv setup_enable nvidia "$setup_enable"
+fi
+if [ "$setup_flatpak" = yes ]; then
+ setup_append_csv setup_enable flatpak "$setup_enable"
+fi
+if [ "$setup_ssh" = yes ]; then
+ setup_append_csv setup_enable ssh "$setup_enable"
+fi
+if [ "$setup_printing" = yes ]; then
+ setup_append_csv setup_enable printing "$setup_enable"
+fi
+case $setup_shell in
+ */zsh|*/fish)
+ setup_append_csv setup_enable shells-extra "$setup_enable"
+ ;;
+esac
+
+# --disable wins
+setup_en_new=
+for setup_item in $setup_enable; do
+ setup_skip=0
+ for setup_d in $setup_disable; do
+ [ "$setup_item" = "$setup_d" ] && setup_skip=1
+ done
+ [ "$setup_skip" -eq 0 ] || continue
+ setup_valid_set "$setup_item" ||
+ setup_fail "$SPS_EX_NOTFOUND" "unknown set: $setup_item"
+ setup_en_new="$setup_en_new $setup_item"
+done
+setup_enable=$(printf '%s' "$setup_en_new" | sed 's/^ //')
+
+setup_pkg_file=$setup_work/packages
+: >"$setup_pkg_file"
+setup_add_pkg()
+{
+ setup_pkg=$1
+ [ -n "$setup_pkg" ] || return 0
+ grep -qx "$setup_pkg" "$setup_pkg_file" 2>/dev/null && return 0
+ printf '%s\n' "$setup_pkg" >>"$setup_pkg_file"
+}
+
+setup_packages_from "$(setup_profile_file "$setup_profile")" |
+ while IFS= read -r setup_pkg; do
+ setup_add_pkg "$setup_pkg"
+ done
+
+# The pipeline subshell cannot update the file... actually it can, the file is
+# in the filesystem. setup_add_pkg appends. Good.
+
+for setup_item in $setup_enable; do
+ setup_packages_from "$(setup_set_file "$setup_item")" |
+ while IFS= read -r setup_pkg; do
+ setup_add_pkg "$setup_pkg"
+ done
+done
+
+for setup_item in $setup_extra; do
+ setup_add_pkg "$setup_item"
+done
+
+setup_write_answers_file()
+{
+ setup_out=$1
+ {
+ printf 'target %s\n' "$setup_target"
+ printf 'profile %s\n' "$setup_profile"
+ printf 'hostname %s\n' "$setup_hostname"
+ printf 'timezone %s\n' "$setup_timezone"
+ printf 'keymap %s\n' "$setup_keymap"
+ printf 'locale %s\n' "$setup_locale"
+ printf 'shell %s\n' "$setup_shell"
+ [ -n "$setup_user" ] && printf 'user %s\n' "$setup_user"
+ [ -n "$setup_user_gecos" ] && printf 'user-gecos %s\n' "$setup_user_gecos"
+ printf 'flatpak %s\n' "$setup_flatpak"
+ printf 'nvidia %s\n' "$setup_nvidia"
+ printf 'ssh %s\n' "$setup_ssh"
+ printf 'printing %s\n' "$setup_printing"
+ printf 'bootloader %s\n' "$setup_bootloader"
+ printf 'update %s\n' "$([ "$setup_do_update" -eq 1 ] && printf yes || printf no)"
+ for setup_item in $setup_enable; do
+ printf 'enable %s\n' "$setup_item"
+ done
+ for setup_item in $setup_extra; do
+ printf 'extra %s\n' "$setup_item"
+ done
+ } >"$setup_out"
+}
+
+setup_summary()
+{
+ printf 'Target: %s\n' "$setup_target"
+ printf 'Profile: %s\n' "$setup_profile"
+ printf 'Hostname: %s\n' "$setup_hostname"
+ printf 'Timezone: %s\n' "$setup_timezone"
+ printf 'Keymap: %s\n' "$setup_keymap"
+ printf 'Locale: %s\n' "$setup_locale"
+ printf 'Shell: %s\n' "$setup_shell"
+ printf 'User: %s\n' "${setup_user:-<none>}"
+ printf 'Sets: %s\n' "${setup_enable:-<none>}"
+ printf 'Extra: %s\n' "${setup_extra:-<none>}"
+ printf 'Flatpak: %s\n' "$setup_flatpak"
+ printf 'NVIDIA: %s\n' "$setup_nvidia"
+ printf 'SSH: %s\n' "$setup_ssh"
+ printf 'Printing: %s\n' "$setup_printing"
+ printf 'Bootloader: %s\n' "$setup_bootloader"
+ printf '\nPackages:\n'
+ sort -u "$setup_pkg_file"
+}
+
+if [ -n "$setup_answers_out" ]; then
+ setup_write_answers_file "$setup_answers_out"
+fi
+
+if [ "$setup_plan" -eq 1 ]; then
+ setup_summary
+ exit 0
+fi
+
+if [ "$setup_interactive" -eq 1 ]; then
+ setup_confirm "Install with this configuration?
+
+$(setup_summary)
+
+Package builds run on this terminal. Source packages may take a long time." ||
+ setup_fail "$SPS_EX_USAGE" "cancelled"
+fi
+
+setup_apply_config()
+{
+ setup_etc=$setup_root/etc
+ mkdir -p "$setup_etc/sps" "$setup_root/root" \
+ "$setup_etc" ||
+ setup_fail "$SPS_EX_PERMISSION" "cannot create target directories"
+
+ printf '%s\n' "$setup_hostname" >"$setup_etc/hostname"
+ printf '%s\n' "$setup_timezone" >"$setup_etc/timezone"
+ printf 'KEYMAP=%s\n' "$setup_keymap" >"$setup_etc/vconsole.conf"
+ printf 'keymap %s\n' "$setup_keymap" >"$setup_etc/sps/keymap"
+ printf 'LANG=%s\n' "$setup_locale" >"$setup_etc/locale.conf"
+
+ if [ -f /usr/share/zoneinfo/"$setup_timezone" ]; then
+ cp /usr/share/zoneinfo/"$setup_timezone" "$setup_etc/localtime" 2>/dev/null || :
+ fi
+
+ setup_write_answers_file "$setup_etc/sps/setup-answers"
+
+ if [ -n "$setup_user" ]; then
+ setup_home=/home/$setup_user
+ mkdir -p "$setup_root$setup_home"
+ if [ ! -f "$setup_etc/passwd" ]; then
+ printf '%s\n' \
+ 'root:x:0:0:root:/root:/bin/sh' \
+ 'nobody:x:65534:65534:Unprivileged User:/:/usr/bin/nologin' \
+ >"$setup_etc/passwd"
+ printf '%s\n' 'root:x:0:' 'nogroup:x:65534:' \
+ >"$setup_etc/group"
+ printf '%s\n' \
+ 'root:!:0:0:99999:7:::' \
+ 'nobody:!:0:0:99999:7:::' \
+ >"$setup_etc/shadow"
+ fi
+ if ! grep -q "^$setup_user:" "$setup_etc/passwd"; then
+ printf '%s:x:1000:1000:%s:%s:%s\n' \
+ "$setup_user" \
+ "${setup_user_gecos:-$setup_user}" \
+ "$setup_home" \
+ "$setup_shell" >>"$setup_etc/passwd"
+ fi
+ if [ -f "$setup_etc/group" ] &&
+ ! grep -q "^$setup_user:" "$setup_etc/group"; then
+ printf '%s:x:1000:\n' "$setup_user" >>"$setup_etc/group"
+ fi
+ if [ -f "$setup_etc/shadow" ] &&
+ ! grep -q "^$setup_user:" "$setup_etc/shadow"; then
+ printf '%s:!:0:0:99999:7:::\n' "$setup_user" >>"$setup_etc/shadow"
+ fi
+ cat >"$setup_root/root/sps-user.txt" <<EOF
+The account '$setup_user' (uid 1000) was recorded in /etc/passwd.
+The password is locked. From a live root run:
+
+ passwd $setup_user
+EOF
+ fi
+
+ cat >"$setup_root/root/sps-bootloader.txt" <<EOF
+SPS does not run grub-install or change EFI variables during setup.
+
+To install GRUB yourself, after chrooting into $setup_root:
+
+ grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=SPS
+ grub-mkconfig -o /boot/grub/grub.cfg
+
+Or for BIOS:
+
+ grub-install /dev/DISK
+ grub-mkconfig -o /boot/grub/grub.cfg
+
+Replace DISK with the actual disk. This is an administrator action.
+EOF
+
+ cat >"$setup_root/root/sps-network.txt" <<EOF
+SPS setup does not write DHCP or static network configuration.
+
+On a Plasma system, use plasma-nm after the first graphical login.
+On a console system, configure the interface by hand, for example with
+ip(8) and a simple /etc/network script of your own.
+
+OpenSSH is installed when the server profile or the ssh set is selected.
+EOF
+
+ cat >"$setup_root/root/sps-fstab.txt" <<EOF
+SPS setup does not partition disks or write /etc/fstab.
+
+Mount the target filesystem on the path you passed to --target (or the
+target screen) before running the installer. Add swap and extra mounts
+yourself after inspecting lsblk and writing /etc/fstab.
+EOF
+}
+
+# Packages first, then hostname/user so filesystem's passwd is already present.
+setup_root=${setup_target%/}
+mkdir -p "$setup_root" ||
+ setup_fail "$SPS_EX_PERMISSION" "cannot create target: $setup_root"
+
+export SPS_ROOT=$setup_root
+
+if [ "$setup_do_update" -eq 1 ]; then
+ printf '%s: updating source repositories\n' "$sps_prog" >&2
+ src update || setup_fail "$SPS_EX_NETWORK" "src update failed"
+fi
+
+setup_pkgs=
+while IFS= read -r setup_pkg; do
+ [ -n "$setup_pkg" ] || continue
+ setup_pkgs="$setup_pkgs $setup_pkg"
+done <<EOF
+$(sort -u "$setup_pkg_file")
+EOF
+
+printf '%s: installing packages:%s\n' "$sps_prog" "$setup_pkgs" >&2
+# sget treats requested names as explicit world roots.
+# shellcheck disable=SC2086
+sget install $setup_pkgs ||
+ setup_fail "$SPS_EX_BUILD" "package installation failed"
+
+# Hostname, user, and notes land after packages so filesystem's passwd wins.
+setup_apply_config
+
+if [ "$setup_flatpak" = yes ]; then
+ setup_fp=$setup_root/usr/bin/flatpak
+ if [ -x "$setup_fp" ]; then
+ printf '%s: adding Flathub remote\n' "$sps_prog" >&2
+ "$setup_fp" --installation=system remote-add --if-not-exists flathub \
+ https://dl.flathub.org/repo/flathub.flatpakrepo \
+ --user=false 2>/dev/null ||
+ SPS_ROOT=$setup_root "$setup_fp" remote-add --if-not-exists \
+ flathub https://dl.flathub.org/repo/flathub.flatpakrepo ||
+ printf '%s: could not add Flathub; add it after first boot\n' \
+ "$sps_prog" >&2
+ else
+ printf '%s: flatpak is not in the target yet; Flathub skipped\n' \
+ "$sps_prog" >&2
+ fi
+fi
+
+if [ "$setup_install_bootloader" -eq 1 ]; then
+ command -v grub-install >/dev/null 2>&1 ||
+ setup_fail "$SPS_EX_PACKAGE" "grub-install is not available"
+ setup_fail "$SPS_EX_USAGE" \
+ "--install-bootloader requires a disk argument; refusing to guess. See /root/sps-bootloader.txt"
+fi
+
+setup_msg "Installation finished.
+
+Target: $setup_root
+Answers: $setup_root/etc/sps/setup-answers
+Bootloader notes: $setup_root/root/sps-bootloader.txt
+
+Set passwords before the first login. If you selected NVIDIA, rebuild the
+kernel modules against the installed kernel."
+
+exit 0
diff --git a/lib/mkiso/initrd-init b/lib/mkiso/initrd-init
new file mode 100755
index 0000000..f245fd5
--- /dev/null
+++ b/lib/mkiso/initrd-init
@@ -0,0 +1,75 @@
+#!/bin/sh
+# Live initramfs. Mount the ISO, squashfs, and overlay, then switch_root.
+
+PATH=/bin:/sbin:/usr/bin:/usr/sbin
+export PATH
+
+/bin/busybox --install -s /bin 2>/dev/null || :
+# Applets may already be relative links from mkiso.
+
+mkdir -p /proc /sys /dev /tmp /mnt/iso /mnt/ro /mnt/rw /mnt/new
+
+mount -t proc proc /proc
+mount -t sysfs sysfs /sys
+mount -t devtmpfs devtmpfs /dev 2>/dev/null || mount -t tmpfs tmpfs /dev
+mkdir -p /dev/pts
+mount -t devpts devpts /dev/pts 2>/dev/null || :
+
+# Load filesystem modules when they are not built in.
+for mod in loop squashfs overlay isofs vfat nls_cp437 nls_iso8859-1 \
+ usb-storage xhci-pci ehci-pci uhci-hcd sr_mod sd_mod ahci nvme \
+ virtio_blk virtio_pci virtio_scsi virtio_net
+do
+ modprobe "$mod" 2>/dev/null || :
+done
+
+sleep 1
+
+found=
+for i in 1 2 3 4 5 6 7 8 9 10; do
+ for dev in /dev/sr0 /dev/sr1 /dev/cdrom \
+ /dev/nvme0n1p1 /dev/nvme1n1p1 \
+ /dev/sda /dev/sda1 /dev/sdb /dev/sdb1 \
+ /dev/vda /dev/vda1 /dev/vdb /dev/vdb1
+ do
+ [ -b "$dev" ] || continue
+ if mount -t iso9660 -o ro "$dev" /mnt/iso 2>/dev/null ||
+ mount -o ro "$dev" /mnt/iso 2>/dev/null; then
+ if [ -f /mnt/iso/live/filesystem.squashfs ]; then
+ found=$dev
+ break
+ fi
+ umount /mnt/iso 2>/dev/null || :
+ fi
+ done
+ [ -n "$found" ] && break
+ sleep 1
+done
+
+if [ -z "$found" ]; then
+ printf '%s\n' "mkiso: cannot find the SPS live filesystem on a CD/USB."
+ printf '%s\n' "Dropping to a shell."
+ exec setsid /bin/sh </dev/console >/dev/console 2>&1
+fi
+
+mount -t squashfs -o ro /mnt/iso/live/filesystem.squashfs /mnt/ro || {
+ printf '%s\n' "mkiso: squashfs mount failed"
+ exec setsid /bin/sh </dev/console >/dev/console 2>&1
+}
+
+mount -t tmpfs tmpfs /mnt/rw
+mkdir -p /mnt/rw/upper /mnt/rw/work
+mount -t overlay overlay \
+ -o lowerdir=/mnt/ro,upperdir=/mnt/rw/upper,workdir=/mnt/rw/work \
+ /mnt/new || {
+ printf '%s\n' "mkiso: overlay mount failed"
+ exec setsid /bin/sh </dev/console >/dev/console 2>&1
+}
+
+mkdir -p /mnt/new/mnt/iso /mnt/new/proc /mnt/new/sys /mnt/new/dev /mnt/new/run /mnt/new/tmp
+mount --move /mnt/iso /mnt/new/mnt/iso 2>/dev/null || :
+mount --move /proc /mnt/new/proc 2>/dev/null || :
+mount --move /sys /mnt/new/sys 2>/dev/null || :
+mount --move /dev /mnt/new/dev 2>/dev/null || :
+
+exec switch_root /mnt/new /sbin/init
diff --git a/lib/mkiso/issue b/lib/mkiso/issue
new file mode 100644
index 0000000..fd669f6
--- /dev/null
+++ b/lib/mkiso/issue
@@ -0,0 +1,3 @@
+SPS Linux live (console)
+
+Mount a destination filesystem on /mnt, then run: setup
diff --git a/lib/mkiso/live-init b/lib/mkiso/live-init
new file mode 100755
index 0000000..491b29b
--- /dev/null
+++ b/lib/mkiso/live-init
@@ -0,0 +1,72 @@
+#!/bin/sh
+# PID 1 on the live squashfs. Bring up consoles and optionally start setup.
+
+PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+if [ -d /usr/lib64/go1.27.0/go/bin ]; then
+ PATH=/usr/lib64/go1.27.0/go/bin:$PATH
+fi
+export PATH
+export HOME=/root
+export TERM=${TERM:-linux}
+export CARGO_HOME=${CARGO_HOME:-/root/.cargo}
+
+mkdir -p /proc /sys /dev /run /tmp /root /mnt /etc/sps
+mount -t proc proc /proc 2>/dev/null || :
+mount -t sysfs sysfs /sys 2>/dev/null || :
+mount -t devtmpfs devtmpfs /dev 2>/dev/null || :
+mkdir -p /dev/pts /dev/shm
+mount -t devpts devpts /dev/pts 2>/dev/null || :
+mount -t tmpfs tmpfs /run 2>/dev/null || :
+mount -t tmpfs tmpfs /tmp 2>/dev/null || :
+
+if command -v ldconfig >/dev/null 2>&1; then
+ ldconfig 2>/dev/null || :
+fi
+
+ln -sf /proc/self/fd /dev/fd 2>/dev/null || :
+ln -sf /proc/self/fd/0 /dev/stdin 2>/dev/null || :
+ln -sf /proc/self/fd/1 /dev/stdout 2>/dev/null || :
+ln -sf /proc/self/fd/2 /dev/stderr 2>/dev/null || :
+
+hostname sps-live 2>/dev/null || :
+[ -f /etc/hostname ] || printf '%s\n' sps-live >/etc/hostname
+
+# Load remaining modules from the live image if present.
+if [ -d /lib/modules ] && command -v depmod >/dev/null 2>&1; then
+ depmod -a 2>/dev/null || :
+fi
+for mod in e1000 e1000e r8169 virtio_net usbnet hid-generic usbhid atkbd
+do
+ modprobe "$mod" 2>/dev/null || :
+done
+
+if command -v ip >/dev/null 2>&1; then
+ ip link set lo up 2>/dev/null || :
+fi
+if command -v dhcpcd >/dev/null 2>&1; then
+ dhcpcd -b 2>/dev/null || :
+elif command -v dhclient >/dev/null 2>&1; then
+ dhclient -nw 2>/dev/null || :
+fi
+
+cat /etc/issue 2>/dev/null || :
+cat /etc/motd 2>/dev/null || :
+
+cmdline=$(cat /proc/cmdline 2>/dev/null || :)
+case " $cmdline " in
+ *" sps=setup "*)
+ if command -v setup >/dev/null 2>&1; then
+ setup || :
+ fi
+ ;;
+esac
+
+# Consoles: a root shell. There is no login prompt on the live image.
+# ttyS0 is for qemu -nographic; tty1 is a hardware VGA console.
+if [ -c /dev/tty1 ]; then
+ setsid /bin/sh -c 'exec /bin/sh <>/dev/tty1 >&0 2>&1' &
+fi
+if [ -c /dev/ttyS0 ]; then
+ exec setsid /bin/sh -c 'exec /bin/sh <>/dev/ttyS0 >&0 2>&1'
+fi
+exec /bin/sh </dev/console >/dev/console 2>&1
diff --git a/lib/mkiso/motd b/lib/mkiso/motd
new file mode 100644
index 0000000..595f2ba
--- /dev/null
+++ b/lib/mkiso/motd
@@ -0,0 +1,20 @@
+SPS Linux live TTY
+
+This disc boots a console. It does not partition disks.
+
+1. Plug in or identify the destination disk (lsblk).
+2. Partition and format it yourself, then mount it on /mnt.
+3. Run: setup --target /mnt
+ Or just: setup
+ and point the target screen at /mnt.
+
+Optional: setup --plan --profile plasma-full --target /mnt
+
+Recipes for sps-core and sps-extra are in /usr/src/sps. Package
+builds run on this live system into the mounted target. A Plasma
+install compiles Qt and KDE from source and takes a long time.
+
+Kernel modules match the kernel on this disc. NVIDIA modules still
+need that kernel after the first boot of the installed system.
+
+Root has an open shell on the live image. Set passwords after install.
diff --git a/lib/mkiso/seed-bins b/lib/mkiso/seed-bins
new file mode 100644
index 0000000..c6b3be3
--- /dev/null
+++ b/lib/mkiso/seed-bins
@@ -0,0 +1,127 @@
+/bin/sh
+/bin/bash
+/bin/busybox
+/usr/bin/dialog
+/usr/bin/git
+/usr/bin/curl
+/usr/bin/wget
+/usr/bin/make
+/usr/bin/gcc
+/usr/bin/g++
+/usr/bin/cc
+/usr/bin/c++
+/usr/bin/as
+/usr/bin/ld
+/usr/bin/ar
+/usr/bin/ranlib
+/usr/bin/nm
+/usr/bin/objdump
+/usr/bin/strip
+/usr/bin/pkgconf
+/usr/bin/pkg-config
+/usr/bin/cmake
+/usr/bin/ninja
+/usr/bin/meson
+/usr/bin/python3
+/usr/bin/python3.12
+/usr/bin/perl
+/usr/bin/rsync
+/usr/bin/tar
+/usr/bin/xz
+/usr/bin/gzip
+/usr/bin/bzip2
+/usr/bin/zstd
+/usr/bin/unzip
+/usr/bin/zip
+/usr/bin/file
+/usr/bin/less
+/usr/bin/nano
+/usr/bin/vim
+/usr/bin/ssh
+/usr/bin/scp
+/usr/bin/openssl
+/usr/sbin/ip
+/sbin/ip
+/bin/ip
+/sbin/modprobe
+/sbin/lsmod
+/sbin/depmod
+/sbin/insmod
+/usr/bin/lsblk
+/usr/bin/blkid
+/usr/sbin/blkid
+/usr/sbin/fdisk
+/usr/sbin/sfdisk
+/usr/sbin/mkfs
+/usr/sbin/mkfs.ext4
+/sbin/mkfs.ext4
+/usr/sbin/mkfs.vfat
+/sbin/mkfs.vfat
+/usr/sbin/mkswap
+/sbin/mkswap
+/usr/bin/mount
+/usr/bin/umount
+/bin/mount
+/bin/umount
+/usr/sbin/swapon
+/usr/sbin/swapoff
+/usr/bin/ls
+/usr/bin/cat
+/usr/bin/cp
+/usr/bin/mv
+/usr/bin/rm
+/usr/bin/mkdir
+/usr/bin/ln
+/usr/bin/chmod
+/usr/bin/chown
+/usr/bin/sed
+/usr/bin/grep
+/usr/bin/gawk
+/usr/bin/find
+/usr/bin/sort
+/usr/bin/head
+/usr/bin/tail
+/usr/bin/tr
+/usr/bin/cut
+/usr/bin/uname
+/usr/bin/tty
+/usr/bin/clear
+/usr/bin/reset
+/usr/bin/sha256sum
+/usr/bin/mktemp
+/usr/bin/install
+/usr/bin/which
+/usr/bin/env
+/usr/bin/ldd
+/usr/sbin/dhcpcd
+/sbin/dhcpcd
+/usr/bin/ping
+/bin/ping
+/usr/bin/dmesg
+/usr/bin/free
+/usr/bin/ps
+/usr/bin/top
+/usr/bin/kill
+/usr/bin/wget
+/usr/bin/curl
+/usr/bin/git
+/usr/bin/patch
+/usr/bin/diff
+/usr/sbin/grub-install
+/usr/bin/grub-mkconfig
+/usr/sbin/grub-mkconfig
+/usr/bin/grub-probe
+/usr/sbin/grub-probe
+/sbin/ldconfig
+/usr/sbin/ldconfig
+/usr/sbin/zic
+/usr/bin/zic
+/usr/bin/localedef
+/usr/bin/locale
+/usr/bin/getent
+/usr/bin/iconv
+/lib64/ld-linux-x86-64.so.2
+/usr/lib64/libgcc_s.so.1
+/usr/lib64/libstdc++.so.6
+/usr/lib64/libgomp.so.1
+/usr/lib64/libatomic.so.1
diff --git a/lib/setup/extras b/lib/setup/extras
new file mode 100644
index 0000000..b6269f9
--- /dev/null
+++ b/lib/setup/extras
@@ -0,0 +1,72 @@
+neovim Neovim
+vim Vim
+nano GNU nano
+emacs GNU Emacs
+helix Helix editor
+kakoune Kakoune editor
+htop htop
+btop btop
+tmux tmux
+sudo sudo
+doas OpenDoas
+zsh Z shell
+fish fish shell
+git Git
+rsync rsync
+openssh OpenSSH
+gnupg GnuPG
+firefox Firefox
+falkon Falkon
+flatpak Flatpak
+pipewire PipeWire
+wget wget
+curl curl
+jq jq
+ripgrep ripgrep
+fd fd
+bat bat
+eza eza
+fzf fzf
+lazygit lazygit
+fastfetch fastfetch
+python Python
+lua Lua
+go Go
+rust Rust
+nodejs Node.js
+perl Perl
+ruby Ruby
+cmake CMake
+meson Meson
+ninja Ninja
+gdb GDB
+strace strace
+lsof lsof
+nmap nmap
+iperf3 iperf3
+cups CUPS printing
+sddm SDDM
+mesa Mesa
+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
+parted GNU parted
+gptfdisk GPT fdisk
+ntfs-3g NTFS driver
+exfatprogs exFAT tools
+mdadm Linux MD RAID
+smartmontools S.M.A.R.T. tools
+bluez BlueZ Bluetooth
+alsa-utils ALSA utilities
+vulkan-tools vulkaninfo
+ethtool ethtool
+nftables nftables
+wireguard-tools WireGuard tools
+
diff --git a/lib/setup/keymaps b/lib/setup/keymaps
new file mode 100644
index 0000000..90f7419
--- /dev/null
+++ b/lib/setup/keymaps
@@ -0,0 +1,21 @@
+us
+uk
+de
+fr
+es
+it
+ru
+br
+dvorak
+colemak
+jp
+kr
+cn
+tr
+pl
+nl
+se
+no
+fi
+dvorak-uk
+us-intl
diff --git a/lib/setup/locales b/lib/setup/locales
new file mode 100644
index 0000000..112bcb7
--- /dev/null
+++ b/lib/setup/locales
@@ -0,0 +1,32 @@
+C.UTF-8
+en_US.UTF-8
+en_GB.UTF-8
+de_DE.UTF-8
+fr_FR.UTF-8
+es_ES.UTF-8
+it_IT.UTF-8
+pt_BR.UTF-8
+pt_PT.UTF-8
+nl_NL.UTF-8
+pl_PL.UTF-8
+ru_RU.UTF-8
+uk_UA.UTF-8
+sv_SE.UTF-8
+nb_NO.UTF-8
+da_DK.UTF-8
+fi_FI.UTF-8
+cs_CZ.UTF-8
+hu_HU.UTF-8
+ro_RO.UTF-8
+tr_TR.UTF-8
+el_GR.UTF-8
+ja_JP.UTF-8
+ko_KR.UTF-8
+zh_CN.UTF-8
+zh_TW.UTF-8
+ar_SA.UTF-8
+he_IL.UTF-8
+hi_IN.UTF-8
+th_TH.UTF-8
+vi_VN.UTF-8
+id_ID.UTF-8
diff --git a/lib/setup/profiles/minimal.profile b/lib/setup/profiles/minimal.profile
new file mode 100644
index 0000000..5501cc7
--- /dev/null
+++ b/lib/setup/profiles/minimal.profile
@@ -0,0 +1,5 @@
+name minimal
+title Minimal console system
+description Core userland without a graphical environment.
+package base-system
+package dialog
diff --git a/lib/setup/profiles/plasma-desktop.profile b/lib/setup/profiles/plasma-desktop.profile
new file mode 100644
index 0000000..f25d095
--- /dev/null
+++ b/lib/setup/profiles/plasma-desktop.profile
@@ -0,0 +1,14 @@
+name plasma-desktop
+title Plasma Desktop
+description KDE Plasma desktop session without the full application suite.
+package base-system
+package dialog
+package plasma-desktop
+package sddm
+package dejavu-fonts
+package mesa
+package pipewire
+package wireplumber
+package xdg-desktop-portal
+package xdg-desktop-portal-kde
+package power-profiles-daemon
diff --git a/lib/setup/profiles/plasma-full.profile b/lib/setup/profiles/plasma-full.profile
new file mode 100644
index 0000000..6a82ed0
--- /dev/null
+++ b/lib/setup/profiles/plasma-full.profile
@@ -0,0 +1,17 @@
+name plasma-full
+title Plasma Full
+description Complete Plasma desktop plus KDE applications.
+package base-system
+package dialog
+package plasma-full
+package sddm
+package dejavu-fonts
+package mesa
+package pipewire
+package wireplumber
+package xdg-desktop-portal
+package xdg-desktop-portal-kde
+package power-profiles-daemon
+package kde-apps
+package firefox
+package flatpak
diff --git a/lib/setup/profiles/server.profile b/lib/setup/profiles/server.profile
new file mode 100644
index 0000000..adb9a7c
--- /dev/null
+++ b/lib/setup/profiles/server.profile
@@ -0,0 +1,9 @@
+name server
+title Server
+description Minimal system plus OpenSSH, GnuPG, and rsync.
+package base-system
+package dialog
+package openssh
+package gnupg
+package rsync
+package sudo
diff --git a/lib/setup/sets/bluetooth.set b/lib/setup/sets/bluetooth.set
new file mode 100644
index 0000000..e4996cd
--- /dev/null
+++ b/lib/setup/sets/bluetooth.set
@@ -0,0 +1,5 @@
+name bluetooth
+title Bluetooth
+description BlueZ stack. Plasma Bluetooth UI comes with plasma-full.
+default 0
+package bluez
diff --git a/lib/setup/sets/browsers.set b/lib/setup/sets/browsers.set
new file mode 100644
index 0000000..72ab03d
--- /dev/null
+++ b/lib/setup/sets/browsers.set
@@ -0,0 +1,5 @@
+name browsers
+title Web browsers
+description Firefox. Falkon is also pulled in by kde-apps.
+default 0
+package firefox
diff --git a/lib/setup/sets/containers.set b/lib/setup/sets/containers.set
new file mode 100644
index 0000000..94ec15f
--- /dev/null
+++ b/lib/setup/sets/containers.set
@@ -0,0 +1,7 @@
+name containers
+title Containers
+description bubblewrap sandboxing. Docker/Podman are not part of the base set.
+default 0
+package bubblewrap
+package slirp4netns
+package fuse-overlayfs
diff --git a/lib/setup/sets/devel-tools.set b/lib/setup/sets/devel-tools.set
new file mode 100644
index 0000000..abaa202
--- /dev/null
+++ b/lib/setup/sets/devel-tools.set
@@ -0,0 +1,5 @@
+name devel-tools
+title Development tools
+description Compilers' companions: cmake, meson, ninja, gdb, pkgconf, and autotools.
+default 0
+package devel-tools
diff --git a/lib/setup/sets/editors-extra.set b/lib/setup/sets/editors-extra.set
new file mode 100644
index 0000000..3d8f61c
--- /dev/null
+++ b/lib/setup/sets/editors-extra.set
@@ -0,0 +1,7 @@
+name editors-extra
+title Extra editors
+description Emacs, Helix, and Kakoune in addition to nano/vim/neovim.
+default 0
+package emacs
+package helix
+package kakoune
diff --git a/lib/setup/sets/firmware.set b/lib/setup/sets/firmware.set
new file mode 100644
index 0000000..7ac51fd
--- /dev/null
+++ b/lib/setup/sets/firmware.set
@@ -0,0 +1,5 @@
+name firmware
+title Firmware
+description linux-firmware blob collection. Needed for many NICs and GPUs.
+default 0
+package linux-firmware
diff --git a/lib/setup/sets/flatpak.set b/lib/setup/sets/flatpak.set
new file mode 100644
index 0000000..0371803
--- /dev/null
+++ b/lib/setup/sets/flatpak.set
@@ -0,0 +1,9 @@
+name flatpak
+title Flatpak
+description Flatpak, bubblewrap, and the Flathub remote (added after install if selected).
+default 0
+package flatpak
+package bubblewrap
+package xdg-dbus-proxy
+package ostree
+package xdg-desktop-portal
diff --git a/lib/setup/sets/fonts-extra.set b/lib/setup/sets/fonts-extra.set
new file mode 100644
index 0000000..9eaf652
--- /dev/null
+++ b/lib/setup/sets/fonts-extra.set
@@ -0,0 +1,8 @@
+name fonts-extra
+title Extra fonts
+description Additional fonts beyond the DejaVu set used by Plasma.
+default 0
+package liberation-fonts
+package noto-fonts
+package noto-fonts-cjk
+package noto-fonts-emoji
diff --git a/lib/setup/sets/kde-apps.set b/lib/setup/sets/kde-apps.set
new file mode 100644
index 0000000..e778882
--- /dev/null
+++ b/lib/setup/sets/kde-apps.set
@@ -0,0 +1,5 @@
+name kde-apps
+title KDE applications
+description Dolphin, Konsole, Kate, Ark, Okular, Gwenview, Spectacle, and other KDE apps.
+default 0
+package kde-apps
diff --git a/lib/setup/sets/languages.set b/lib/setup/sets/languages.set
new file mode 100644
index 0000000..0056ed0
--- /dev/null
+++ b/lib/setup/sets/languages.set
@@ -0,0 +1,5 @@
+name languages
+title Language toolchains
+description Python, Lua, Go, Rust, Node.js, Perl, and Ruby interpreters and compilers.
+default 0
+package languages
diff --git a/lib/setup/sets/multimedia.set b/lib/setup/sets/multimedia.set
new file mode 100644
index 0000000..97f9016
--- /dev/null
+++ b/lib/setup/sets/multimedia.set
@@ -0,0 +1,13 @@
+name multimedia
+title Multimedia
+description PipeWire, WirePlumber, FFmpeg, and desktop audio/video libraries.
+default 0
+package pipewire
+package wireplumber
+package ffmpeg
+package gstreamer
+package gst-plugins-base
+package gst-plugins-good
+package alsa-lib
+package alsa-utils
+package vulkan-loader
diff --git a/lib/setup/sets/network-extra.set b/lib/setup/sets/network-extra.set
new file mode 100644
index 0000000..81d6f77
--- /dev/null
+++ b/lib/setup/sets/network-extra.set
@@ -0,0 +1,12 @@
+name network-extra
+title Extra networking
+description nmap, traceroute helpers, bind-tools, and wget/curl already in core.
+default 0
+package nmap
+package traceroute
+package bind-tools
+package iperf3
+package wireshark-cli
+package ethtool
+package nftables
+package wireguard-tools
diff --git a/lib/setup/sets/nvidia.set b/lib/setup/sets/nvidia.set
new file mode 100644
index 0000000..f24703f
--- /dev/null
+++ b/lib/setup/sets/nvidia.set
@@ -0,0 +1,7 @@
+name nvidia
+title NVIDIA drivers
+description NVIDIA open kernel modules and user-space driver. Requires a matching kernel.
+default 0
+package nvidia-open
+package nvidia-utils
+package nvidia-settings
diff --git a/lib/setup/sets/office.set b/lib/setup/sets/office.set
new file mode 100644
index 0000000..aa813b4
--- /dev/null
+++ b/lib/setup/sets/office.set
@@ -0,0 +1,8 @@
+name office
+title Office and documents
+description Okular, Ark, and related document tools. LibreOffice is not packaged here.
+default 0
+package okular
+package ark
+package gwenview
+package spectacle
diff --git a/lib/setup/sets/power.set b/lib/setup/sets/power.set
new file mode 100644
index 0000000..e584422
--- /dev/null
+++ b/lib/setup/sets/power.set
@@ -0,0 +1,8 @@
+name power
+title Power management
+description UPower, power-profiles-daemon, and related session helpers.
+default 1
+package upower
+package power-profiles-daemon
+package polkit
+package elogind
diff --git a/lib/setup/sets/printing.set b/lib/setup/sets/printing.set
new file mode 100644
index 0000000..13e9912
--- /dev/null
+++ b/lib/setup/sets/printing.set
@@ -0,0 +1,7 @@
+name printing
+title Printing
+description CUPS and related printing support.
+default 0
+package cups
+package cups-filters
+package print-manager
diff --git a/lib/setup/sets/qol-cli.set b/lib/setup/sets/qol-cli.set
new file mode 100644
index 0000000..d822105
--- /dev/null
+++ b/lib/setup/sets/qol-cli.set
@@ -0,0 +1,5 @@
+name qol-cli
+title Quality-of-life CLI
+description Fast search, listings, git helpers, and system monitors.
+default 1
+package qol-cli
diff --git a/lib/setup/sets/shells-extra.set b/lib/setup/sets/shells-extra.set
new file mode 100644
index 0000000..80f02bb
--- /dev/null
+++ b/lib/setup/sets/shells-extra.set
@@ -0,0 +1,6 @@
+name shells-extra
+title Extra shells
+description zsh and fish in addition to bash.
+default 0
+package zsh
+package fish
diff --git a/lib/setup/sets/ssh.set b/lib/setup/sets/ssh.set
new file mode 100644
index 0000000..b208ac5
--- /dev/null
+++ b/lib/setup/sets/ssh.set
@@ -0,0 +1,5 @@
+name ssh
+title OpenSSH server
+description OpenSSH client and server. On by default for the server profile.
+default 0
+package openssh
diff --git a/lib/setup/sets/storage.set b/lib/setup/sets/storage.set
new file mode 100644
index 0000000..8d4bd65
--- /dev/null
+++ b/lib/setup/sets/storage.set
@@ -0,0 +1,16 @@
+name storage
+title Disk tools
+description ext4, FAT, XFS, Btrfs, LVM, and cryptsetup.
+default 0
+package e2fsprogs
+package dosfstools
+package xfsprogs
+package btrfs-progs
+package lvm2
+package cryptsetup
+package mdadm
+package ntfs-3g
+package exfatprogs
+package parted
+package gptfdisk
+package smartmontools
diff --git a/lib/setup/sets/wifi.set b/lib/setup/sets/wifi.set
new file mode 100644
index 0000000..c7fb703
--- /dev/null
+++ b/lib/setup/sets/wifi.set
@@ -0,0 +1,7 @@
+name wifi
+title Wireless
+description iwd, wpa_supplicant, and iw.
+default 0
+package iwd
+package wpa_supplicant
+package iw
diff --git a/lib/setup/shells b/lib/setup/shells
new file mode 100644
index 0000000..e4c43c8
--- /dev/null
+++ b/lib/setup/shells
@@ -0,0 +1,4 @@
+/bin/sh
+/bin/bash
+/usr/bin/zsh
+/usr/bin/fish
diff --git a/lib/setup/timezones b/lib/setup/timezones
new file mode 100644
index 0000000..ed2c5e3
--- /dev/null
+++ b/lib/setup/timezones
@@ -0,0 +1,28 @@
+UTC
+America/Los_Angeles
+America/Denver
+America/Chicago
+America/New_York
+America/Sao_Paulo
+America/Mexico_City
+America/Toronto
+Europe/London
+Europe/Berlin
+Europe/Paris
+Europe/Madrid
+Europe/Rome
+Europe/Amsterdam
+Europe/Warsaw
+Europe/Moscow
+Europe/Istanbul
+Africa/Cairo
+Africa/Johannesburg
+Asia/Tokyo
+Asia/Seoul
+Asia/Shanghai
+Asia/Singapore
+Asia/Kolkata
+Asia/Dubai
+Australia/Sydney
+Australia/Melbourne
+Pacific/Auckland
diff --git a/man/mkiso.1 b/man/mkiso.1
new file mode 100644
index 0000000..daedc93
--- /dev/null
+++ b/man/mkiso.1
@@ -0,0 +1,79 @@
+.TH MKISO 1 "2026-08-31" "SPS 1.0" "SPS Commands"
+.SH NAME
+mkiso \- build an SPS live TTY ISO
+.SH SYNOPSIS
+.nf
+mkiso --output FILE.iso [options]
+mkiso --layout DIR [options]
+.fi
+.SH DESCRIPTION
+.B mkiso
+builds a hybrid ISO that boots to a text console with
+.BR setup (1)
+on PATH. Flash it to USB, boot, mount a destination filesystem on
+.IR /mnt ,
+and run
+.B setup .
+.PP
+The image does not partition disks and never runs
+.B grub-install
+on a hard disk.
+.B --layout
+writes the live root only, for tests and inspection.
+.PP
+Host compiler tools can be copied into the image so
+.B setup
+can build packages from the bundled recipes. Pass
+.B --no-seed
+to omit them.
+.SH OPTIONS
+.TP
+.BI --output " file"
+Write a bootable ISO.
+.TP
+.BI --layout " dir"
+Populate a live root and exit.
+.TP
+.BI --busybox " file"
+Static busybox used for the initramfs and live
+.BR /bin/busybox .
+.TP
+.BI --kernel " file"
+Live kernel. Defaults to
+.IR /boot/vmlinuz .
+.TP
+.BI --core " dir"
+.TP
+.BI --extra " dir"
+Copy recipe trees to
+.I /usr/src/sps/core
+and
+.IR /usr/src/sps/extra .
+.TP
+.B --with-rust
+.TP
+.B --with-go
+Copy those host toolchains into the live image.
+.TP
+.B --with-firmware
+Copy
+.I /lib/firmware
+into the squashfs. This tree is often more than a gigabyte, so published
+ISOs omit it. Wireless adapters that need blobs will need firmware copied
+onto the target after install, or a locally built image with this flag.
+.TP
+.B --no-modules
+Do not copy the full
+.I /lib/modules
+tree into the squashfs. The initramfs still gets squashfs/overlay/USB
+modules so the disc can boot.
+.TP
+.B --no-seed
+Do not copy gcc, Python, and related host tools.
+.SH FILES
+.I $SPS_LIBDIR/mkiso
+holds the live init scripts, motd, and seed binary list.
+.SH SEE ALSO
+.BR setup (1),
+.BR sget (1),
+.BR src (1)
diff --git a/man/setup.1 b/man/setup.1
new file mode 100644
index 0000000..48a76a5
--- /dev/null
+++ b/man/setup.1
@@ -0,0 +1,173 @@
+.TH SETUP 1 "2026-08-31" "SPS 1.0" "SPS Commands"
+.SH NAME
+setup \- dialog-based SPS system installer
+.SH SYNOPSIS
+.nf
+setup [options]
+setup --list-profiles
+setup --list-sets
+setup --plan [options]
+.fi
+.SH DESCRIPTION
+.B setup
+installs an SPS system into a target root. Interactive use is a sequence of
+.BR dialog (1)
+screens in the same spirit as the Slackware and FreeBSD installers: profile,
+optional package sets, extra packages, locale, hostname, timezone, keymap,
+first user, login shell, then a visible package build.
+.PP
+Every screen has a command-line equivalent. Scripts and tests should pass
+.B --non-interactive
+or
+.B --plan .
+.B --plan
+does not start
+.BR dialog (1).
+.PP
+The installer does not partition disks, format filesystems, or run
+.B grub-install
+unless
+.B --install-bootloader
+is given, and even then it refuses to guess a disk. Bootloader instructions
+are written to
+.I /root/sps-bootloader.txt
+in the target.
+.SH PROFILES
+.TP
+.B minimal
+.B base-system
+and
+.BR dialog (1).
+.TP
+.B server
+Minimal plus OpenSSH, GnuPG, rsync, and sudo.
+.TP
+.B plasma-desktop
+KDE Plasma session without the full application suite.
+.TP
+.B plasma-full
+Plasma plus
+.BR kde-apps ,
+Firefox, and Flatpak.
+.SH SETS
+Optional sets are listed by
+.B --list-sets
+and stored as text files under
+.I $SPS_LIBDIR/setup/sets .
+They include quality-of-life CLI tools, development tools, language
+toolchains, KDE applications, NVIDIA drivers, Flatpak, power management,
+multimedia, extra network tools, extra editors, container helpers, printing,
+browsers, extra shells, extra fonts, office tools, and OpenSSH.
+.PP
+.B plasma-desktop
+and
+.B plasma-full
+turn the power and qol-cli sets on by default.
+.B --enable
+adds sets.
+.B --disable
+removes them.
+.SH OPTIONS
+.TP
+.BI --target " dir"
+Installation root. Must be an absolute path. Becomes
+.B SPS_ROOT
+for
+.BR src (1)
+and
+.BR sget (1).
+.TP
+.BI --profile " name"
+One of the profiles above.
+.TP
+.BI --enable " set[,set...]"
+.TP
+.BI --disable " set[,set...]"
+.TP
+.BI --hostname " name"
+.TP
+.BI --timezone " zone"
+.TP
+.BI --keymap " map"
+.TP
+.BI --locale " name"
+.B LANG
+written to
+.I /etc/locale.conf .
+Default
+.BR C.UTF-8 .
+.TP
+.BI --shell " path"
+Login shell for the first user. Default
+.IR /bin/sh .
+Choosing zsh or fish also enables the
+.B shells-extra
+set.
+.TP
+.BI --extra " pkg[,pkg...]"
+Individual packages in addition to the profile and sets.
+.TP
+.B --ssh yes|no
+Enable the OpenSSH set.
+.TP
+.B --printing yes|no
+Enable CUPS and related printing packages.
+.TP
+.BI --user " name"
+First unprivileged account (uid 1000). The password is left locked; see
+.I /root/sps-user.txt .
+.TP
+.B --flatpak yes|no
+Install the Flatpak set and attempt to add Flathub after packages land.
+.TP
+.B --nvidia yes|no
+Install NVIDIA open kernel modules and utilities. A matching kernel is
+required; this installer does not build one.
+.TP
+.B --bootloader none|grub
+Write notes only.
+.TP
+.B --install-bootloader
+Refuses to guess a disk. See the notes file.
+.TP
+.B --no-update
+Skip
+.B src update
+before
+.B sget install .
+.TP
+.B --non-interactive
+Never start
+.BR dialog (1).
+.TP
+.B --plan
+Print the resolved configuration and package list, then exit.
+.TP
+.BI --write-answers " file"
+.TP
+.BI --read-answers " file"
+Options override values from the file.
+.TP
+.B --list-profiles
+.TP
+.B --list-sets
+.SH FILES
+.TP
+.I $SPS_LIBDIR/setup
+Profiles, sets, keymaps, and timezones.
+.TP
+.I $SPS_SETUP_DIR
+Optional override of that directory, used by tests.
+.TP
+.I /etc/sps/setup-answers
+Copy of the resolved answers in the target root.
+.SH EXIT STATUS
+Returns 0 on success, 2 for usage errors, 3 when a profile or set is unknown,
+8 for permission problems, 9 when
+.B src update
+fails, and 10 when package installation fails.
+.SH SEE ALSO
+.BR dialog (1),
+.BR sget (1),
+.BR src (1),
+.BR pkin (1)
diff --git a/tests/setup-fixture/extras b/tests/setup-fixture/extras
new file mode 100644
index 0000000..d1cb581
--- /dev/null
+++ b/tests/setup-fixture/extras
@@ -0,0 +1 @@
+hello Hello fixture
diff --git a/tests/setup-fixture/keymaps b/tests/setup-fixture/keymaps
new file mode 100644
index 0000000..2dc5cb6
--- /dev/null
+++ b/tests/setup-fixture/keymaps
@@ -0,0 +1,2 @@
+us
+de
diff --git a/tests/setup-fixture/locales b/tests/setup-fixture/locales
new file mode 100644
index 0000000..89c58c7
--- /dev/null
+++ b/tests/setup-fixture/locales
@@ -0,0 +1,2 @@
+C.UTF-8
+en_US.UTF-8
diff --git a/tests/setup-fixture/profiles/tiny.profile b/tests/setup-fixture/profiles/tiny.profile
new file mode 100644
index 0000000..fbdc10a
--- /dev/null
+++ b/tests/setup-fixture/profiles/tiny.profile
@@ -0,0 +1,4 @@
+name tiny
+title Tiny test profile
+description Fixture profile
+package hello
diff --git a/tests/setup-fixture/sets/extra.set b/tests/setup-fixture/sets/extra.set
new file mode 100644
index 0000000..4ac2dc0
--- /dev/null
+++ b/tests/setup-fixture/sets/extra.set
@@ -0,0 +1,5 @@
+name extra
+title Extra fixture set
+description Fixture set
+default 0
+package extra-pkg
diff --git a/tests/setup-fixture/shells b/tests/setup-fixture/shells
new file mode 100644
index 0000000..de0249b
--- /dev/null
+++ b/tests/setup-fixture/shells
@@ -0,0 +1,2 @@
+/bin/sh
+/bin/bash
diff --git a/tests/setup-fixture/timezones b/tests/setup-fixture/timezones
new file mode 100644
index 0000000..3578a16
--- /dev/null
+++ b/tests/setup-fixture/timezones
@@ -0,0 +1,2 @@
+UTC
+America/Los_Angeles
diff --git a/tests/test_mkiso.sh b/tests/test_mkiso.sh
new file mode 100755
index 0000000..c36c714
--- /dev/null
+++ b/tests/test_mkiso.sh
@@ -0,0 +1,52 @@
+#!/bin/sh
+# mkiso --help and --layout
+
+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_mkiso: %s\n' "$*" >&2; exit 1; }
+
+mkiso=$project_dir/bin/mkiso
+export SPS_LIBDIR=$project_dir/lib
+export PATH=$project_dir/bin:$PATH
+
+help=$("$mkiso" --help)
+case $help in
+ *'--output'*) ;;
+ *) fail 'help missing --output' ;;
+esac
+case $help in
+ *'--with-firmware'*) ;;
+ *) fail 'help missing --with-firmware' ;;
+esac
+
+tmp=$(mktemp -d "${TMPDIR:-/tmp}/sps-test-mkiso.XXXXXX") || fail mktemp
+trap 'rm -rf "$tmp"' 0 HUP INT TERM
+
+layout=$tmp/live
+bb=
+if [ -x /tmp/sps-wave/busybox.static ]; then
+ bb=/tmp/sps-wave/busybox.static
+fi
+
+if [ -n "$bb" ]; then
+ "$mkiso" --layout "$layout" --no-seed --no-modules --busybox "$bb" \
+ >/dev/null
+else
+ "$mkiso" --layout "$layout" --no-seed --no-modules >/dev/null
+fi
+
+[ -f "$layout/etc/sps/live" ] || fail 'live marker missing'
+[ -f "$layout/etc/motd" ] || fail 'motd missing'
+[ -x "$layout/sbin/init" ] || fail 'live init missing'
+[ -x "$layout/usr/bin/setup" ] || fail 'setup not installed into layout'
+grep -q 'setup' "$layout/etc/motd" || fail 'motd does not mention setup'
+[ -f "$layout/etc/sps/repos.conf" ] || fail 'repos.conf missing'
+if [ -x "$layout/bin/busybox" ] && [ -L "$layout/bin/mkdir" ]; then
+ target=$(readlink "$layout/bin/mkdir")
+ case $target in
+ busybox|./busybox) ;;
+ *) fail "busybox applet mkdir should be a relative link, got $target" ;;
+ esac
+fi
diff --git a/tests/test_setup.sh b/tests/test_setup.sh
new file mode 100755
index 0000000..c9db617
--- /dev/null
+++ b/tests/test_setup.sh
@@ -0,0 +1,133 @@
+#!/bin/sh
+# setup: plan, answers, validation, and a fixture install.
+
+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: %s\n' "$*" >&2; exit 1; }
+contains() { case $1 in *"$2"*) : ;; *) fail "expected output to contain '$2'" ;; esac; }
+
+setup=$project_dir/bin/setup
+export SPS_LIBDIR=$project_dir/lib
+export PATH=$project_dir/bin:$PATH
+
+tmp=$(mktemp -d "${TMPDIR:-/tmp}/sps-test-setup.XXXXXX") || fail mktemp
+trap 'rm -rf "$tmp"' 0 HUP INT TERM
+
+profiles=$("$setup" --list-profiles)
+contains "$profiles" 'minimal'
+contains "$profiles" 'plasma-desktop'
+contains "$profiles" 'plasma-full'
+contains "$profiles" 'server'
+
+sets=$("$setup" --list-sets)
+contains "$sets" 'printing'
+contains "$sets" 'browsers'
+contains "$sets" 'shells-extra'
+contains "$sets" 'ssh'
+contains "$sets" 'storage'
+contains "$sets" 'wifi'
+contains "$sets" 'firmware'
+contains "$sets" 'bluetooth'
+
+plan=$("$setup" --plan --target /mnt --profile minimal --disable qol-cli)
+contains "$plan" 'Profile: minimal'
+contains "$plan" 'base-system'
+contains "$plan" 'dialog'
+contains "$plan" 'Locale: C.UTF-8'
+
+plan=$("$setup" --plan --target /mnt --profile plasma-desktop --user alex \
+ --hostname darkstar --timezone UTC --keymap us --nvidia yes \
+ --locale en_US.UTF-8 --shell /bin/bash --extra neovim,htop \
+ --ssh yes --printing yes)
+contains "$plan" 'Hostname: darkstar'
+contains "$plan" 'User: alex'
+contains "$plan" 'Locale: en_US.UTF-8'
+contains "$plan" 'Shell: /bin/bash'
+contains "$plan" 'plasma-desktop'
+contains "$plan" 'nvidia-open'
+contains "$plan" 'qol-cli'
+contains "$plan" 'neovim'
+contains "$plan" 'htop'
+contains "$plan" 'openssh'
+contains "$plan" 'cups'
+
+"$setup" --plan --target /mnt --profile server \
+ --write-answers "$tmp/answers" >/dev/null
+grep -qx 'profile server' "$tmp/answers" || fail 'answers missing profile'
+grep -qx 'target /mnt' "$tmp/answers" || fail 'answers missing target'
+
+plan=$("$setup" --plan --read-answers "$tmp/answers" --hostname other)
+contains "$plan" 'Hostname: other'
+contains "$plan" 'Profile: server'
+
+set +e
+"$setup" --plan --target /mnt --profile no-such-profile >/dev/null 2>"$tmp/err"
+st=$?
+set -e
+[ "$st" -eq 3 ] || fail "unknown profile returned $st"
+contains "$(cat "$tmp/err")" 'unknown profile'
+
+set +e
+"$setup" --plan --target mnt --profile minimal >/dev/null 2>"$tmp/err"
+st=$?
+set -e
+[ "$st" -eq 2 ] || fail "relative target returned $st"
+
+set +e
+"$setup" --plan --target /mnt --profile minimal --hostname 'bad host' \
+ >/dev/null 2>"$tmp/err"
+st=$?
+set -e
+[ "$st" -eq 2 ] || fail "bad hostname returned $st"
+
+# Fixture profile install into a disposable root.
+export SPS_SETUP_DIR=$project_dir/tests/setup-fixture
+root=$tmp/root
+db=$tmp/db
+cache=$tmp/cache
+build=$tmp/build
+repo=$tmp/repo
+mkdir -p "$root" "$db" "$cache" "$build" "$repo/hello" "$repo/extra-pkg"
+: >"$tmp/sps.conf"
+printf 'dir test %s 10\n' "$repo" >"$tmp/repos.conf"
+export SPS_ROOT=$root SPS_DB=$db SPS_CACHE=$cache SPS_BUILD=$build
+export SPS_CONFIG=$tmp/sps.conf SPS_REPOS_CONFIG=$tmp/repos.conf
+
+cat >"$repo/hello/recipe" <<'EOF'
+name hello
+version 1.0
+release 1
+arch any
+description fixture hello
+install mkdir -p "$PKG/usr/share/sps-test"
+install printf '%s\n' hello >"$PKG/usr/share/sps-test/hello"
+EOF
+cat >"$repo/extra-pkg/recipe" <<'EOF'
+name extra-pkg
+version 1.0
+release 1
+arch any
+description fixture extra
+install mkdir -p "$PKG/usr/share/sps-test"
+install printf '%s\n' extra >"$PKG/usr/share/sps-test/extra"
+EOF
+
+src update >/dev/null
+"$setup" --non-interactive --no-update --target "$root" --profile tiny \
+ --hostname fixture --timezone UTC --keymap us --user tester \
+ --enable extra --user-gecos 'Test User' >/dev/null
+
+[ "$(cat "$root/usr/share/sps-test/hello")" = hello ] || fail 'hello missing'
+[ "$(cat "$root/usr/share/sps-test/extra")" = extra ] || fail 'extra missing'
+[ "$(cat "$root/etc/hostname")" = fixture ] || fail 'hostname not written'
+[ "$(cat "$root/etc/locale.conf")" = 'LANG=C.UTF-8' ] || fail 'locale not written'
+grep -q '^tester:x:1000:1000:Test User:/home/tester:/bin/sh$' \
+ "$root/etc/passwd" || fail 'user not recorded'
+[ -f "$root/etc/sps/setup-answers" ] || fail 'setup-answers missing'
+[ -f "$root/root/sps-bootloader.txt" ] || fail 'bootloader notes missing'
+[ -f "$root/root/sps-network.txt" ] || fail 'network notes missing'
+[ -f "$root/root/sps-fstab.txt" ] || fail 'fstab notes missing'
+grep -q 'grub-install' "$root/root/sps-bootloader.txt" ||
+ fail 'bootloader notes omitted grub-install warning'