From 0ea8b713ddd1c4a92944d7b8f1fa35e9a629557a Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Fri, 21 Aug 2026 20:24:09 -0500 Subject: [PATCH] feat: graphical installer in the live ISO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ISO now boots straight into a GTK installer instead of dropping to a tty. Structure: - installer/arcline-installer: small GTK3 (Python) frontend that drives scripts/deploy-disk.sh — pick a disk, choose boot mode, type the device path to confirm, watch the deploy log, reboot. Pure helper logic is tested against lsblk (lowercase keys, pseudo-devices filtered). - scripts/build-live.sh: builds build/rootfs/-live by cloning the CLEAN rootfs and layering on live-boot, a minimal X session (Xorg + openbox), the installer, and the deploy tooling under /usr/lib/arcline (deploy-disk.sh + btrfs/init.sh + edition fstabs, laid out so the scripts' own path resolution works unchanged). - overlays/live/: arcline-installer.service + session script that start Xorg on vt1 (with -allow-root) and run the installer as the X client. - build-iso.sh: builds the live rootfs for the squashfs AND stages the clean rootfs archive into isofiles/install/ — the installer deploys the clean archive, so what's installed is the hardened system, never the live session with the installer in it. - Refactor: ARCLINE_LIVE handling removed from build-rootfs.sh and configure-system.sh (now lives entirely in build-live.sh). - validate.sh now checks overlays shell scripts + installer python. - docs updated (building.md, architecture.md, installer/README.md). --- docs/architecture.md | 9 +- docs/building.md | 17 +- installer/README.md | 71 +++++ installer/arcline-installer | 260 ++++++++++++++++++ .../systemd/system/arcline-installer.service | 16 ++ .../usr/local/sbin/arcline-installer-session | 11 + scripts/build-iso.sh | 65 +++-- scripts/build-live.sh | 97 +++++++ scripts/build-rootfs.sh | 3 +- scripts/configure-system.sh | 8 - scripts/validate.sh | 11 +- 11 files changed, 522 insertions(+), 46 deletions(-) create mode 100644 installer/README.md create mode 100755 installer/arcline-installer create mode 100644 overlays/live/etc/systemd/system/arcline-installer.service create mode 100755 overlays/live/usr/local/sbin/arcline-installer-session create mode 100755 scripts/build-live.sh diff --git a/docs/architecture.md b/docs/architecture.md index 7ac417b..a58045f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -33,11 +33,13 @@ Pipeline stages live in `scripts/`: |-------|--------|--------------| | bootstrap | `build-rootfs.sh` | debootstrap minbase, apt sources, package install | | overlay | `apply-overlays.sh` | copies `overlays/base` + `overlays/` into the rootfs | -| configure | `configure-system.sh` | runs *in the chroot*: hostname, locale, kernel cmdline, services, live-boot, toolchain | -| package | `build-iso.sh` | kernel + initramfs + squashfs → hybrid BIOS/UEFI ISO | +| configure | `configure-system.sh` | runs *in the chroot*: hostname, locale, kernel cmdline, services, toolchain | +| live | `build-live.sh` | clone clean rootfs → live session (live-boot + X + graphical installer) | +| package | `build-iso.sh` | live rootfs → hybrid BIOS/UEFI ISO, plus the clean install archive | | image | `build-image.sh` | rootfs → bootable qcow2/raw disk image (via `deploy-disk.sh`) | | deploy | `deploy-disk.sh` | partition → btrfs layout → copy rootfs → GRUB + fstab (shared by image + installer) | | install | `install.sh` | scripted installer for a real disk (confirmation-gated) | +| GUI install | `installer/arcline-installer` | GTK frontend booted by the live ISO (drives `deploy-disk.sh`) | | orchestrate | `build-edition.sh` / `Makefile` | wire the above to `make iso-` | ## The source trees @@ -75,6 +77,9 @@ The four original follow-up items are implemented: observability stack installs without upstream repos. 4. **Secure boot** — MOK-based signing (`scripts/secureboot/`), off by default, enabled with `ARCLINE_SIGN=1`. +5. **Graphical installer** — the live ISO now boots straight into a GTK + installer (`installer/arcline-installer`) that drives `deploy-disk.sh`, + instead of dropping to a tty. ## Still on the horizon diff --git a/docs/building.md b/docs/building.md index f3842c5..f468d3c 100644 --- a/docs/building.md +++ b/docs/building.md @@ -116,10 +116,19 @@ independent of the toolchain and always included. ## Notes on the ISO -The ISO boots a **live** system (via `live-boot`): the rootfs is compressed to -a squashfs and mounted on boot, so you can try an edition before installing it -to disk. The same rootfs can be installed with the btrfs layout via -`btrfs/init.sh` (the installer is follow-up work — see `docs/architecture.md`). +The ISO is a **graphical installer** medium: it boots to a minimal live +session (Xorg) that auto-starts the Arcline installer window. Pick a target +disk, confirm the device path, and it installs to disk with the btrfs layout, +GRUB, and a real fstab — driven by the same `deploy-disk.sh` core as the +scripted installer. + +The live session is built by `build-live.sh` on top of the **clean** rootfs; +the ISO carries that clean rootfs as the install source (`install/`), so what +lands on disk is the hardened installed system — never the live session with +the installer in it. + +Install a user + desktop afterwards if you want a graphical login +(workstation edition already ships KDE Plasma + SDDM and boots to it). ### Burning the ISO diff --git a/installer/README.md b/installer/README.md new file mode 100644 index 0000000..eb936ae --- /dev/null +++ b/installer/README.md @@ -0,0 +1,71 @@ +# Arcline graphical installer + +`installer/arcline-installer` is the GTK3 frontend that the live ISO boots +into. It is a thin wrapper around `scripts/deploy-disk.sh` — the same, +auditable deploy core the scripted installer and disk-image builder use. + +## How it works + +``` + boot ISO → live session (Xorg on vt1) → installer window + pick target disk → confirm device path → deploy-disk.sh → reboot +``` + +The installer extracts `install/arcline-.tar.xz` (the *clean* rootfs +archive staged on the ISO by `build-iso.sh`) to a temp dir and runs: + +``` +/usr/lib/arcline/scripts/deploy-disk.sh --boot +``` + +streaming the deploy log into the window. Everything destructive lives in +`deploy-disk.sh`, which the live image ships (with its dependencies) at +`/usr/lib/arcline`: + +``` +/usr/lib/arcline/scripts/{common,deploy-disk,apply-fstab}.sh +/usr/lib/arcline/btrfs/init.sh +/usr/lib/arcline/editions/{server,workstation,cloud}/fstab +``` + +The layout mirrors the repo so the scripts' own path resolution works +unchanged. + +## Building the ISO + +```bash +make iso-server # or iso-workstation / iso-cloud +``` + +`build-iso.sh` builds the clean rootfs, then `build-live.sh` clones it into +`build/rootfs/-live` and layers on live-boot, a minimal X session +(openbox), the installer, and the deploy tooling. The live session rootfs is +**never** what gets installed — the installer always deploys the clean +archive, so what you end up with on disk is exactly the hardened system, not +the live session with the installer in it. + +## Autostart + +`overlays/live/etc/systemd/system/arcline-installer.service` starts +`arcline-installer-session` on boot: it launches Xorg on vt1 (with +`-allow-root`, since the live session runs as root) and the installer as the +X client. When the installer exits, X and the session end. + +## Manual run (for development / on a box without the ISO) + +```bash +# on any Arcline system with the deploy tooling: +sudo /usr/local/bin/arcline-installer +``` + +The app refuses to run as a non-root user and errors if the deploy tooling is +missing. + +## Notes + +- Disk discovery uses `lsblk` (util-linux); read-only devices are hidden. +- Confirmation mirrors `scripts/install.sh`: you must type the exact device + path before the install button will proceed. +- Root is hidden by design; the installer runs before any user session. +- For UEFI machines, pick "UEFI" boot mode (or use `BOOT=efi` when building); + see `docs/secureboot.md` for signing the boot chain for Secure Boot. diff --git a/installer/arcline-installer b/installer/arcline-installer new file mode 100755 index 0000000..cde8629 --- /dev/null +++ b/installer/arcline-installer @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +# ───────────────────────────────────────────────────────────────────────────── +# Arcline OS — graphical installer (runs in the live session) +# +# A small GTK3 frontend for scripts/deploy-disk.sh. It boots to the console +# of the live ISO and walks through: pick a target disk -> confirm (type the +# device path, like scripts/install.sh) -> install, streaming the deploy log +# into the window. Everything destructive is delegated to deploy-disk.sh, +# which is shipped in the live image at /usr/lib/arcline. +# +# Requires root (the live session runs as root). No network needed. +# ───────────────────────────────────────────────────────────────────────────── +import glob +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import GLib, Gtk + +DEPLOY = "/usr/lib/arcline/scripts/deploy-disk.sh" +ARCHIVE_CANDIDATES = ( + "/install/*.tar.xz", + "/run/live/medium/install/*.tar.xz", + "/lib/live/mount/medium/install/*.tar.xz", + "/media/*/install/*.tar.xz", + "/mnt/*/install/*.tar.xz", +) + + +def find_archive(): + for pattern in ARCHIVE_CANDIDATES: + hits = sorted(glob.glob(pattern)) + if hits: + return hits[0] + return None + + +def current_edition(): + try: + with open("/etc/arcline-release", encoding="utf-8") as fh: + for line in fh: + m = re.match(r"Edition:\s*(\S+)", line) + if m: + return m.group(1) + except OSError: + pass + return "server" + + +def list_disks(): + try: + out = subprocess.run( + ["lsblk", "-J", "-o", "NAME,SIZE,TYPE,MODEL,RM,RO"], + capture_output=True, text=True, check=True, timeout=10, + ).stdout + data = json.loads(out) + except (subprocess.SubprocessError, json.JSONDecodeError): + return [] + disks = [] + for d in data.get("blockdevices", []): + if d.get("type") != "disk" or d.get("ro"): + continue + name = d.get("name", "") + # hide pseudo-devices that are never install targets + if name.startswith(("zram", "loop", "ram")): + continue + disks.append(d) + return disks + + +class Installer(Gtk.Window): + def __init__(self): + super().__init__(title="Arcline OS Installer") + self.set_default_size(760, 640) + self.proc = None + self.tmpdir = None + self.install_ok = False + + self.archive = find_archive() + self.edition = current_edition() + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + box.set_margin_top(16) + box.set_margin_bottom(16) + box.set_margin_start(20) + box.set_margin_end(20) + self.add(box) + + title = Gtk.Label() + title.set_markup(f"Arcline OS Installer — {self.edition} edition") + box.pack_start(title, False, False, 0) + + # install source + if self.archive: + src = Gtk.Label() + src.set_halign(Gtk.Align.START) + src.set_markup(f"Installing from: {self.archive}") + box.pack_start(src, False, False, 0) + else: + warn = Gtk.Label(label="WARNING: no install archive found (expected /install/*.tar.xz on the ISO)") + warn.set_name("warn") + box.pack_start(warn, False, False, 0) + + # target disk + self.disk_combo = Gtk.ComboBoxText() + self.disks = list_disks() + for d in self.disks: + model = (d.get("model") or "").strip() + removable = "(removable)" if d.get("rm") else "" + self.disk_combo.append_text(f"/dev/{d['name']} {d.get('size','?')} {model} {removable}".strip()) + if not self.disks: + self.disk_combo.append_text("(no writable disks found)") + self.disk_combo.set_active(0) + row_disk = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + row_disk.pack_start(Gtk.Label(label="Target disk:"), False, False, 0) + row_disk.pack_start(self.disk_combo, True, True, 0) + box.pack_start(row_disk, False, False, 0) + + # boot mode + self.boot_combo = Gtk.ComboBoxText() + for label, val in (("BIOS (grub-pc)", "bios"), ("UEFI (grub-efi-amd64)", "efi")): + self.boot_combo.append(label, val) + self.boot_combo.set_active(0) + row_boot = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + row_boot.pack_start(Gtk.Label(label="Boot mode:"), False, False, 0) + row_boot.pack_start(self.boot_combo, True, True, 0) + box.pack_start(row_boot, False, False, 0) + + # danger + confirm + danger = Gtk.Label() + danger.set_name("warn") + danger.set_markup("⚠ ALL DATA on the target disk will be destroyed.") + box.pack_start(danger, False, False, 0) + + row_confirm = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + row_confirm.pack_start(Gtk.Label(label="Type the device path to confirm:"), False, False, 0) + self.confirm = Gtk.Entry() + self.confirm.set_placeholder_text("/dev/sda") + row_confirm.pack_start(self.confirm, True, True, 0) + box.pack_start(row_confirm, False, False, 0) + + # install button + self.install_btn = Gtk.Button(label="Install Arcline OS") + self.install_btn.set_sensitive(self.archive is not None and bool(self.disks)) + self.install_btn.connect("clicked", self.on_install) + box.pack_start(self.install_btn, False, False, 0) + + # output + self.output = Gtk.TextView() + self.output.set_editable(False) + self.output.set_monospace(True) + scroller = Gtk.ScrolledWindow() + scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scroller.set_vexpand(True) + scroller.add(self.output) + box.pack_start(scroller, True, True, 0) + + # status + reboot + self.status = Gtk.Label(label="") + box.pack_start(self.status, False, False, 0) + self.reboot_btn = Gtk.Button(label="Reboot") + self.reboot_btn.set_sensitive(False) + self.reboot_btn.connect("clicked", lambda *_: subprocess.Popen(["systemctl", "reboot"])) + box.pack_start(self.reboot_btn, False, False, 0) + + self.connect("destroy", Gtk.main_quit) + + # ── helpers ───────────────────────────────────────────────────────────── + def append(self, text): + buf = self.output.get_buffer() + buf.insert(buf.get_end_iter(), text) + + def selected_device(self): + i = self.disk_combo.get_active() + if i < 0 or i >= len(self.disks): + return None + return f"/dev/{self.disks[i]['name']}" + + def boot_mode(self): + return self.boot_combo.get_active_id() or "bios" + + # ── install ───────────────────────────────────────────────────────────── + def on_install(self, _btn): + dev = self.selected_device() + if not dev: + self.status.set_text("Select a target disk first.") + return + if self.confirm.get_text().strip() != dev: + self.status.set_markup(f"Type {dev} exactly to confirm.") + return + + self.install_btn.set_sensitive(False) + self.status.set_text("Preparing…") + self.tmpdir = tempfile.mkdtemp(prefix="arcline-install-") + try: + shutil.unpack_archive(self.archive, self.tmpdir, format="xztar") + except Exception as exc: # noqa: BLE001 + self.status.set_markup(f"Failed to extract archive: {exc}") + self.install_btn.set_sensitive(True) + return + + cmd = [DEPLOY, dev, self.tmpdir, self.edition, "--boot", self.boot_mode()] + self.append(f"$ {' '.join(cmd)}\n") + self.proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1, + ) + self.status.set_text("Installing… this takes a while.") + GLib.timeout_add(100, self.drain) + + def drain(self): + if self.proc is None: + return False + line = self.proc.stdout.readline() + if line: + self.append(line) + # keep the tail visible + adj = self.output.get_parent().get_vadjustment() + adj.set_value(adj.get_upper()) + return True + rc = self.proc.poll() + if rc is None: + return True + self.proc.stdout.close() + self.proc = None + if rc == 0: + self.install_ok = True + self.status.set_markup("Install complete. Remove the media and reboot.") + self.reboot_btn.set_sensitive(True) + else: + self.status.set_markup(f"Install failed (exit {rc}). See log above.") + self.install_btn.set_sensitive(True) + if self.tmpdir: + shutil.rmtree(self.tmpdir, ignore_errors=True) + self.tmpdir = None + return False + + +def main(): + if os.geteuid() != 0: + print("error: the Arcline installer must run as root", file=sys.stderr) + return 1 + if not os.path.exists(DEPLOY): + print(f"error: {DEPLOY} not found — installer is incomplete", file=sys.stderr) + return 1 + win = Installer() + win.show_all() + Gtk.main() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/overlays/live/etc/systemd/system/arcline-installer.service b/overlays/live/etc/systemd/system/arcline-installer.service new file mode 100644 index 0000000..94bfd68 --- /dev/null +++ b/overlays/live/etc/systemd/system/arcline-installer.service @@ -0,0 +1,16 @@ +[Unit] +Description=Arcline graphical installer (live session) +Documentation=docs/building.md +After=systemd-user-sessions.service systemd-logind.service +Conflicts=getty@tty1.service + +[Service] +Type=simple +ExecStart=/usr/local/sbin/arcline-installer-session +StandardInput=tty +TTYPath=/dev/tty1 +TTYReset=yes +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/overlays/live/usr/local/sbin/arcline-installer-session b/overlays/live/usr/local/sbin/arcline-installer-session new file mode 100755 index 0000000..a2af0c3 --- /dev/null +++ b/overlays/live/usr/local/sbin/arcline-installer-session @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Arcline OS — live session: start X on vt1 and launch the graphical installer. +# Runs as root in the live session; Xorg needs -allow-root for that. +set -euo pipefail + +export DISPLAY=:0 +# clear any stale lock from a previous session in the same live boot +rm -f /tmp/.X0-lock /tmp/.X11-unix/X0 + +exec xinit /usr/local/bin/arcline-installer -- \ + /usr/bin/Xorg -allow-root -nolisten tcp -keeptty :0 vt1 diff --git a/scripts/build-iso.sh b/scripts/build-iso.sh index 7005c05..974f1be 100755 --- a/scripts/build-iso.sh +++ b/scripts/build-iso.sh @@ -1,14 +1,17 @@ #!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── -# Arcline OS — live ISO builder +# Arcline OS — live ISO builder (with graphical installer) # # scripts/build-iso.sh # -# Wraps a rootfs into a hybrid (BIOS+UEFI) live ISO: -# 1. ensure the rootfs exists (build it with live-boot support if needed) -# 2. stage kernel + initramfs + squashfs in isofiles/live -# 3. write the grub boot config (live-boot: boot=live) -# 4. grub-mkrescue → build/artifacts/arcline---.iso +# Produces a hybrid (BIOS+UEFI) live ISO that boots straight into the Arcline +# graphical installer: +# 1. ensure the CLEAN rootfs exists (what the installer deploys) +# 2. build-live.sh → build/rootfs/-live (live-boot + X + installer) +# 3. stage kernel + initramfs + squashfs in isofiles/live, and the clean +# rootfs archive in isofiles/install (the installer's install source) +# 4. write the grub boot config (live-boot: boot=live) +# 5. grub-mkrescue → build/artifacts/arcline---.iso # # Requires root for the rootfs stage; the ISO assembly itself runs unprivileged. # ───────────────────────────────────────────────────────────────────────────── @@ -18,48 +21,54 @@ source "$(dirname "${BASH_SOURCE[0]}")/common.sh" EDITION="${1:?usage: build-iso.sh }" validate_edition "$EDITION" -ROOTFS="$ROOTFS_DIR/$EDITION" +CLEAN="$ROOTFS_DIR/$EDITION" +LIVE_ROOT="$ROOTFS_DIR/$EDITION-live" +INSTALL_ARC="$ARTIFACT_DIR/arcline-$EDITION-$VERSION-$ARCH.tar.xz" ISOFILES="$IMAGE_DIR/$EDITION/isofiles" ARTIFACT="$ARTIFACT_DIR/arcline-$EDITION-$VERSION-$ARCH.iso" -# ── 1. rootfs ─────────────────────────────────────────────────────────────── -if [[ ! -d "$ROOTFS" ]]; then - log "rootfs missing — building with live-boot support" - ARCLINE_LIVE=1 "$ROOT/scripts/build-rootfs.sh" "$EDITION" +# ── 1. rootfs(es) ─────────────────────────────────────────────────────────── +# The ISO is a live session (built on top of the clean rootfs) and carries the +# CLEAN rootfs archive as the install source for the graphical installer. +if [[ ! -d "$CLEAN" ]]; then + log "clean rootfs missing — building it first" + "$ROOT/scripts/build-rootfs.sh" "$EDITION" fi - -# live-boot must be present in the rootfs for the ISO to boot -if [[ ! -d "$ROOTFS/lib/live" && ! -d "$ROOTFS/usr/lib/live" ]]; then - warn "rootfs has no live-boot support; rebuilding with ARCLINE_LIVE=1" - ARCLINE_LIVE=1 "$ROOT/scripts/build-rootfs.sh" "$EDITION" +if [[ ! -f "$INSTALL_ARC" ]]; then + log "clean rootfs archive missing — building it first" + "$ROOT/scripts/build-rootfs.sh" "$EDITION" fi +"$ROOT/scripts/build-live.sh" "$EDITION" # ── 2. stage files ────────────────────────────────────────────────────────── log "staging ISO files for edition '$EDITION'" rm -rf "$ISOFILES" -mkdir -p "$ISOFILES/live" "$ISOFILES/boot/grub" +mkdir -p "$ISOFILES/live" "$ISOFILES/boot/grub" "$ISOFILES/install" -KERNEL="$(find "$ROOTFS/boot" -maxdepth 1 -name 'vmlinuz-*' | sort -V | tail -1)" -INITRD="$(find "$ROOTFS/boot" -maxdepth 1 -name 'initrd.img-*' | sort -V | tail -1)" -[[ -n "$KERNEL" && -n "$INITRD" ]] || die "kernel or initramfs not found in rootfs" +KERNEL="$(find "$LIVE_ROOT/boot" -maxdepth 1 -name 'vmlinuz-*' | sort -V | tail -1)" +INITRD="$(find "$LIVE_ROOT/boot" -maxdepth 1 -name 'initrd.img-*' | sort -V | tail -1)" +[[ -n "$KERNEL" && -n "$INITRD" ]] || die "kernel or initramfs not found in live rootfs" cp -L "$KERNEL" "$ISOFILES/live/vmlinuz" cp -L "$INITRD" "$ISOFILES/live/initrd.img" +# the clean (installed-system) rootfs is what the installer deploys +cp "$INSTALL_ARC" "$ISOFILES/install/arcline-$EDITION.tar.xz" + # ── 2b. secure boot (optional: ARCLINE_SIGN=1 + a MOK keypair) ────────────── if [[ "${ARCLINE_SIGN:-0}" == "1" ]]; then log "secure boot: signing boot chain and shipping MOK in the live image" [[ -f "$BUILD_DIR/keys/MOK.der" ]] || die "ARCLINE_SIGN=1 but no MOK keypair — run: scripts/secureboot/gen-keys.sh" - mkdir -p "$ROOTFS/etc/arcline" - cp "$BUILD_DIR/keys/MOK.der" "$ROOTFS/etc/arcline/MOK.der" - "$ROOT/scripts/secureboot/sign-image.sh" "$ROOTFS" --keydir "$BUILD_DIR/keys" + mkdir -p "$LIVE_ROOT/etc/arcline" + cp "$BUILD_DIR/keys/MOK.der" "$LIVE_ROOT/etc/arcline/MOK.der" + "$ROOT/scripts/secureboot/sign-image.sh" "$LIVE_ROOT" --keydir "$BUILD_DIR/keys" "$ROOT/scripts/secureboot/sign-image.sh" "$ISOFILES" --keydir "$BUILD_DIR/keys" fi -log "compressing rootfs → squashfs (this takes a while)" -# The squashfs is the live root. Keep it complete — offline man pages and -# docs are a product promise (see the landing page), so nothing is excluded. -mksquashfs "$ROOTFS" "$ISOFILES/live/arcline.squashfs" -noappend -comp zstd -Xcompression-level 15 2>/dev/null || \ -mksquashfs "$ROOTFS" "$ISOFILES/live/arcline.squashfs" -noappend -comp xz +log "compressing live rootfs → squashfs (this takes a while)" +# The squashfs is the live session root. Keep it complete — offline man pages +# and docs are a product promise (see the landing page), so nothing is excluded. +mksquashfs "$LIVE_ROOT" "$ISOFILES/live/arcline.squashfs" -noappend -comp zstd -Xcompression-level 15 2>/dev/null || \ +mksquashfs "$LIVE_ROOT" "$ISOFILES/live/arcline.squashfs" -noappend -comp xz # ── 3. grub boot config ───────────────────────────────────────────────────── log "writing grub config" diff --git a/scripts/build-live.sh b/scripts/build-live.sh new file mode 100755 index 0000000..3c1648d --- /dev/null +++ b/scripts/build-live.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# Arcline OS — live-session rootfs builder +# +# scripts/build-live.sh +# +# Creates build/rootfs/-live: a bootable live-session rootfs used by +# the ISO. It starts from the CLEAN installed-system rootfs (so what you +# install is exactly what you booted), clones it, and layers on: +# +# - live-boot / live-config / live-tools (boot the squashfs) +# - a minimal X session (Xorg + openbox) for the graphical installer +# - the Arcline graphical installer (installer/arcline-installer) +# - the deploy tooling it needs (deploy-disk.sh + btrfs/init.sh + fstabs) +# +# The live rootfs itself is never what gets installed — the installer deploys +# the clean rootfs archive (staged into the ISO by build-iso.sh). +# ───────────────────────────────────────────────────────────────────────────── +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" + +EDITION="${1:?usage: build-live.sh }" +validate_edition "$EDITION" +require_root "$0" "$@" + +CLEAN="$ROOTFS_DIR/$EDITION" +LIVE_ROOT="$ROOTFS_DIR/$EDITION-live" + +# ── 1. ensure the clean rootfs ────────────────────────────────────────────── +if [[ ! -d "$CLEAN" ]]; then + log "clean rootfs missing — building it first" + "$ROOT/scripts/build-rootfs.sh" "$EDITION" +fi + +log "═══ building live session rootfs for $EDITION ═══" +rm -rf "$LIVE_ROOT" +cp -a "$CLEAN" "$LIVE_ROOT" + +# ── chroot helpers ────────────────────────────────────────────────────────── +mount_pseudo() { + mount --bind /dev "$LIVE_ROOT/dev" + mount --bind /proc "$LIVE_ROOT/proc" + mount --bind /sys "$LIVE_ROOT/sys" + mount --bind /dev/pts "$LIVE_ROOT/dev/pts" 2>/dev/null || true +} +unmount_pseudo() { + umount -l "$LIVE_ROOT/dev/pts" 2>/dev/null || true + umount -l "$LIVE_ROOT/sys" 2>/dev/null || true + umount -l "$LIVE_ROOT/proc" 2>/dev/null || true + umount -l "$LIVE_ROOT/dev" 2>/dev/null || true +} +trap 'unmount_pseudo' EXIT +chroot_run() { chroot "$LIVE_ROOT" /bin/bash -c "$*"; } + +# ── 2. install live session packages ──────────────────────────────────────── +log "[1/4] installing live-boot + X + installer dependencies" +mount_pseudo +chroot_run "export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y --no-install-recommends \ + live-boot live-config-systemd live-tools \ + xserver-xorg xinit x11-xserver-utils openbox \ + python3-gi gir1.2-gtk-3.0 gdisk parted dosfstools \ + | tee /tmp/live-packages.log" + +# ── 3. ship the graphical installer + deploy tooling ──────────────────────── +log "[2/4] installing installer + deploy tooling" +install -Dm0755 "$ROOT/installer/arcline-installer" "$LIVE_ROOT/usr/local/bin/arcline-installer" + +# The installer drives deploy-disk.sh; mirror the repo layout under +# /usr/lib/arcline so common.sh's ROOT resolution works unchanged. +install -Dm0755 "$ROOT/scripts/common.sh" "$LIVE_ROOT/usr/lib/arcline/scripts/common.sh" +install -Dm0755 "$ROOT/scripts/deploy-disk.sh" "$LIVE_ROOT/usr/lib/arcline/scripts/deploy-disk.sh" +install -Dm0755 "$ROOT/scripts/apply-fstab.sh" "$LIVE_ROOT/usr/lib/arcline/scripts/apply-fstab.sh" +install -Dm0755 "$ROOT/btrfs/init.sh" "$LIVE_ROOT/usr/lib/arcline/btrfs/init.sh" +for e in "${EDITIONS[@]}"; do + install -Dm0644 "$ROOT/editions/$e/fstab" "$LIVE_ROOT/usr/lib/arcline/editions/$e/fstab" +done + +# ── 4. live overlay (session autostart) + service wiring ──────────────────── +log "[3/4] applying live overlay" +cp -a "$ROOT/overlays/live/." "$LIVE_ROOT/" + +chroot_run "systemctl enable arcline-installer.service 2>/dev/null || true +systemctl mask sddm 2>/dev/null || true +systemctl set-default multi-user.target 2>/dev/null || true" + +# the live session is disposable; give it a fresh identity +echo "arclines-live" > "$LIVE_ROOT/etc/hostname" +rm -f "$LIVE_ROOT/etc/machine-id" +: > "$LIVE_ROOT/etc/machine-id" + +log "[4/4] cleaning live rootfs" +chroot_run "apt-get clean 2>/dev/null; rm -rf /var/lib/apt/lists/* /var/cache/apt/* /tmp/* /root/.bash_history" +unmount_pseudo + +log "live rootfs ready: $LIVE_ROOT" diff --git a/scripts/build-rootfs.sh b/scripts/build-rootfs.sh index e13ffc6..96abc49 100755 --- a/scripts/build-rootfs.sh +++ b/scripts/build-rootfs.sh @@ -40,7 +40,6 @@ esac EDIR="$(edition_dir "$EDITION")" ROOTFS="$ROOTFS_DIR/$EDITION" ARTIFACT="$ARTIFACT_DIR/arcline-$EDITION-$VERSION-$ARCH.tar.xz" -LIVE="${ARCLINE_LIVE:-0}" mkdir -p "$ROOTFS_DIR" "$LOG_DIR" "$ARTIFACT_DIR" rm -rf "$ROOTFS" "$ARTIFACT" @@ -135,7 +134,7 @@ case "$ARCLINE_TOOLCHAIN" in esac mount_pseudo -chroot_run "ARCLINE_EDITION_DIR='/root/arcline-edition' ARCLINE_LIVE='$LIVE' ARCLINE_LOCK_ROOT='${ARCLINE_LOCK_ROOT:-0}' ARCLINE_EXTRA_REPOS='${ARCLINE_EXTRA_REPOS:-0}' /root/configure-system.sh '$EDITION'" +chroot_run "ARCLINE_EDITION_DIR='/root/arcline-edition' ARCLINE_LOCK_ROOT='${ARCLINE_LOCK_ROOT:-0}' ARCLINE_EXTRA_REPOS='${ARCLINE_EXTRA_REPOS:-0}' /root/configure-system.sh '$EDITION'" rm -f "$ROOTFS/root/configure-system.sh" rm -rf "$ROOTFS/root/arcline-edition" "$ROOTFS/arcline-debs" unmount_pseudo diff --git a/scripts/configure-system.sh b/scripts/configure-system.sh index df1ad48..dc0c55f 100755 --- a/scripts/configure-system.sh +++ b/scripts/configure-system.sh @@ -17,7 +17,6 @@ EDITION="${1:?usage: configure-system.sh }" DISTRO_NAME="${DISTRO_NAME:-Arcline OS}" VERSION="${VERSION:-0.1.0}" RELEASE_NAME="${RELEASE_NAME:-arclines}" -LIVE="${ARCLINE_LIVE:-0}" LOCK_ROOT="${ARCLINE_LOCK_ROOT:-0}" EXTRA_REPOS="${ARCLINE_EXTRA_REPOS:-0}" @@ -85,13 +84,6 @@ fi # ensure no package telemetry survives. rm -f /var/log/apt/*.log /var/cache/apt/archives/*.deb -# ── live-boot (ISO builds) ────────────────────────────────────────────────── -if [[ "$LIVE" == "1" ]]; then - log "installing live-boot support" - export DEBIAN_FRONTEND=noninteractive - apt-get install -y --no-install-recommends live-boot live-config-systemd live-tools || warn "live-boot install failed" -fi - # ── Arcline toolchain (host-built .debs) ──────────────────────────────────── if [[ -d /arcline-debs ]] && ls /arcline-debs/*.deb >/dev/null 2>&1; then log "installing Arcline toolchain packages" diff --git a/scripts/validate.sh b/scripts/validate.sh index a70f1eb..1d5e21a 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -21,8 +21,15 @@ while IFS= read -r -d '' s; do if ! bash -n "$s"; then note_fail "syntax error in $s" fi -done < <(find "$ROOT/scripts" "$ROOT/btrfs" "$ROOT/toolchain" "$ROOT/tests" \ - -name '*.sh' -type f -print0 2>/dev/null) +done < <(find "$ROOT/scripts" "$ROOT/btrfs" "$ROOT/toolchain" "$ROOT/tests" "$ROOT/overlays" \ + -type f \( -name '*.sh' -o -name 'arcline-installer-session' \) -print0 2>/dev/null) + +log "validating installer (python syntax)…" +if command -v python3 >/dev/null 2>&1; then + python3 -m py_compile "$ROOT/installer/arcline-installer" 2>/dev/null \ + || note_fail "python syntax error in installer/arcline-installer" + rm -rf "$ROOT/installer/__pycache__" +fi log "validating editions…" for e in "${EDITIONS[@]}"; do