feat: graphical installer in the live ISO

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/<edition>-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).
This commit is contained in:
Blake Ridgway
2026-08-21 20:24:09 -05:00
parent 578c518553
commit 0ea8b713dd
11 changed files with 522 additions and 46 deletions

View File

@@ -33,11 +33,13 @@ Pipeline stages live in `scripts/`:
|-------|--------|--------------| |-------|--------|--------------|
| bootstrap | `build-rootfs.sh` | debootstrap minbase, apt sources, package install | | bootstrap | `build-rootfs.sh` | debootstrap minbase, apt sources, package install |
| overlay | `apply-overlays.sh` | copies `overlays/base` + `overlays/<edition>` into the rootfs | | overlay | `apply-overlays.sh` | copies `overlays/base` + `overlays/<edition>` into the rootfs |
| configure | `configure-system.sh` | runs *in the chroot*: hostname, locale, kernel cmdline, services, live-boot, toolchain | | configure | `configure-system.sh` | runs *in the chroot*: hostname, locale, kernel cmdline, services, toolchain |
| package | `build-iso.sh` | kernel + initramfs + squashfs → hybrid BIOS/UEFI ISO | | 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`) | | 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) | | 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) | | 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-<edition>` | | orchestrate | `build-edition.sh` / `Makefile` | wire the above to `make iso-<edition>` |
## The source trees ## The source trees
@@ -75,6 +77,9 @@ The four original follow-up items are implemented:
observability stack installs without upstream repos. observability stack installs without upstream repos.
4. **Secure boot** — MOK-based signing (`scripts/secureboot/`), off by 4. **Secure boot** — MOK-based signing (`scripts/secureboot/`), off by
default, enabled with `ARCLINE_SIGN=1`. 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 ## Still on the horizon

View File

@@ -116,10 +116,19 @@ independent of the toolchain and always included.
## Notes on the ISO ## Notes on the ISO
The ISO boots a **live** system (via `live-boot`): the rootfs is compressed to The ISO is a **graphical installer** medium: it boots to a minimal live
a squashfs and mounted on boot, so you can try an edition before installing it session (Xorg) that auto-starts the Arcline installer window. Pick a target
to disk. The same rootfs can be installed with the btrfs layout via disk, confirm the device path, and it installs to disk with the btrfs layout,
`btrfs/init.sh` (the installer is follow-up work — see `docs/architecture.md`). 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 ### Burning the ISO

71
installer/README.md Normal file
View File

@@ -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-<edition>.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 <device> <rootfs> <edition> --boot <bios|efi>
```
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/<edition>-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.

260
installer/arcline-installer Executable file
View File

@@ -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"<big><b>Arcline OS Installer</b> — {self.edition} edition</big>")
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"<small>Installing from: <tt>{self.archive}</tt></small>")
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("⚠ <b>ALL DATA on the target disk will be destroyed.</b>")
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"<span color='red'>Type <b>{dev}</b> exactly to confirm.</span>")
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"<span color='red'>Failed to extract archive: {exc}</span>")
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("<span color='green'>Install complete. Remove the media and reboot.</span>")
self.reboot_btn.set_sensitive(True)
else:
self.status.set_markup(f"<span color='red'>Install failed (exit {rc}). See log above.</span>")
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())

View File

@@ -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

View File

@@ -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

View File

@@ -1,14 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — live ISO builder # Arcline OS — live ISO builder (with graphical installer)
# #
# scripts/build-iso.sh <edition> # scripts/build-iso.sh <edition>
# #
# Wraps a rootfs into a hybrid (BIOS+UEFI) live ISO: # Produces a hybrid (BIOS+UEFI) live ISO that boots straight into the Arcline
# 1. ensure the rootfs exists (build it with live-boot support if needed) # graphical installer:
# 2. stage kernel + initramfs + squashfs in isofiles/live # 1. ensure the CLEAN rootfs exists (what the installer deploys)
# 3. write the grub boot config (live-boot: boot=live) # 2. build-live.sh → build/rootfs/<edition>-live (live-boot + X + installer)
# 4. grub-mkrescue → build/artifacts/arcline-<edition>-<version>-<arch>.iso # 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-<edition>-<version>-<arch>.iso
# #
# Requires root for the rootfs stage; the ISO assembly itself runs unprivileged. # 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 <edition>}" EDITION="${1:?usage: build-iso.sh <edition>}"
validate_edition "$EDITION" 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" ISOFILES="$IMAGE_DIR/$EDITION/isofiles"
ARTIFACT="$ARTIFACT_DIR/arcline-$EDITION-$VERSION-$ARCH.iso" ARTIFACT="$ARTIFACT_DIR/arcline-$EDITION-$VERSION-$ARCH.iso"
# ── 1. rootfs ─────────────────────────────────────────────────────────────── # ── 1. rootfs(es) ───────────────────────────────────────────────────────────
if [[ ! -d "$ROOTFS" ]]; then # The ISO is a live session (built on top of the clean rootfs) and carries the
log "rootfs missing — building with live-boot support" # CLEAN rootfs archive as the install source for the graphical installer.
ARCLINE_LIVE=1 "$ROOT/scripts/build-rootfs.sh" "$EDITION" if [[ ! -d "$CLEAN" ]]; then
log "clean rootfs missing — building it first"
"$ROOT/scripts/build-rootfs.sh" "$EDITION"
fi fi
if [[ ! -f "$INSTALL_ARC" ]]; then
# live-boot must be present in the rootfs for the ISO to boot log "clean rootfs archive missing — building it first"
if [[ ! -d "$ROOTFS/lib/live" && ! -d "$ROOTFS/usr/lib/live" ]]; then "$ROOT/scripts/build-rootfs.sh" "$EDITION"
warn "rootfs has no live-boot support; rebuilding with ARCLINE_LIVE=1"
ARCLINE_LIVE=1 "$ROOT/scripts/build-rootfs.sh" "$EDITION"
fi fi
"$ROOT/scripts/build-live.sh" "$EDITION"
# ── 2. stage files ────────────────────────────────────────────────────────── # ── 2. stage files ──────────────────────────────────────────────────────────
log "staging ISO files for edition '$EDITION'" log "staging ISO files for edition '$EDITION'"
rm -rf "$ISOFILES" 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)" KERNEL="$(find "$LIVE_ROOT/boot" -maxdepth 1 -name 'vmlinuz-*' | sort -V | tail -1)"
INITRD="$(find "$ROOTFS/boot" -maxdepth 1 -name 'initrd.img-*' | 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 rootfs" [[ -n "$KERNEL" && -n "$INITRD" ]] || die "kernel or initramfs not found in live rootfs"
cp -L "$KERNEL" "$ISOFILES/live/vmlinuz" cp -L "$KERNEL" "$ISOFILES/live/vmlinuz"
cp -L "$INITRD" "$ISOFILES/live/initrd.img" 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) ────────────── # ── 2b. secure boot (optional: ARCLINE_SIGN=1 + a MOK keypair) ──────────────
if [[ "${ARCLINE_SIGN:-0}" == "1" ]]; then if [[ "${ARCLINE_SIGN:-0}" == "1" ]]; then
log "secure boot: signing boot chain and shipping MOK in the live image" 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" [[ -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" mkdir -p "$LIVE_ROOT/etc/arcline"
cp "$BUILD_DIR/keys/MOK.der" "$ROOTFS/etc/arcline/MOK.der" cp "$BUILD_DIR/keys/MOK.der" "$LIVE_ROOT/etc/arcline/MOK.der"
"$ROOT/scripts/secureboot/sign-image.sh" "$ROOTFS" --keydir "$BUILD_DIR/keys" "$ROOT/scripts/secureboot/sign-image.sh" "$LIVE_ROOT" --keydir "$BUILD_DIR/keys"
"$ROOT/scripts/secureboot/sign-image.sh" "$ISOFILES" --keydir "$BUILD_DIR/keys" "$ROOT/scripts/secureboot/sign-image.sh" "$ISOFILES" --keydir "$BUILD_DIR/keys"
fi fi
log "compressing rootfs → squashfs (this takes a while)" log "compressing live rootfs → squashfs (this takes a while)"
# The squashfs is the live root. Keep it complete — offline man pages and # The squashfs is the live session root. Keep it complete — offline man pages
# docs are a product promise (see the landing page), so nothing is excluded. # 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 "$LIVE_ROOT" "$ISOFILES/live/arcline.squashfs" -noappend -comp zstd -Xcompression-level 15 2>/dev/null || \
mksquashfs "$ROOTFS" "$ISOFILES/live/arcline.squashfs" -noappend -comp xz mksquashfs "$LIVE_ROOT" "$ISOFILES/live/arcline.squashfs" -noappend -comp xz
# ── 3. grub boot config ───────────────────────────────────────────────────── # ── 3. grub boot config ─────────────────────────────────────────────────────
log "writing grub config" log "writing grub config"

97
scripts/build-live.sh Executable file
View File

@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — live-session rootfs builder
#
# scripts/build-live.sh <edition>
#
# Creates build/rootfs/<edition>-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 <edition>}"
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"

View File

@@ -40,7 +40,6 @@ esac
EDIR="$(edition_dir "$EDITION")" EDIR="$(edition_dir "$EDITION")"
ROOTFS="$ROOTFS_DIR/$EDITION" ROOTFS="$ROOTFS_DIR/$EDITION"
ARTIFACT="$ARTIFACT_DIR/arcline-$EDITION-$VERSION-$ARCH.tar.xz" ARTIFACT="$ARTIFACT_DIR/arcline-$EDITION-$VERSION-$ARCH.tar.xz"
LIVE="${ARCLINE_LIVE:-0}"
mkdir -p "$ROOTFS_DIR" "$LOG_DIR" "$ARTIFACT_DIR" mkdir -p "$ROOTFS_DIR" "$LOG_DIR" "$ARTIFACT_DIR"
rm -rf "$ROOTFS" "$ARTIFACT" rm -rf "$ROOTFS" "$ARTIFACT"
@@ -135,7 +134,7 @@ case "$ARCLINE_TOOLCHAIN" in
esac esac
mount_pseudo 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 -f "$ROOTFS/root/configure-system.sh"
rm -rf "$ROOTFS/root/arcline-edition" "$ROOTFS/arcline-debs" rm -rf "$ROOTFS/root/arcline-edition" "$ROOTFS/arcline-debs"
unmount_pseudo unmount_pseudo

View File

@@ -17,7 +17,6 @@ EDITION="${1:?usage: configure-system.sh <edition>}"
DISTRO_NAME="${DISTRO_NAME:-Arcline OS}" DISTRO_NAME="${DISTRO_NAME:-Arcline OS}"
VERSION="${VERSION:-0.1.0}" VERSION="${VERSION:-0.1.0}"
RELEASE_NAME="${RELEASE_NAME:-arclines}" RELEASE_NAME="${RELEASE_NAME:-arclines}"
LIVE="${ARCLINE_LIVE:-0}"
LOCK_ROOT="${ARCLINE_LOCK_ROOT:-0}" LOCK_ROOT="${ARCLINE_LOCK_ROOT:-0}"
EXTRA_REPOS="${ARCLINE_EXTRA_REPOS:-0}" EXTRA_REPOS="${ARCLINE_EXTRA_REPOS:-0}"
@@ -85,13 +84,6 @@ fi
# ensure no package telemetry survives. # ensure no package telemetry survives.
rm -f /var/log/apt/*.log /var/cache/apt/archives/*.deb 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) ──────────────────────────────────── # ── Arcline toolchain (host-built .debs) ────────────────────────────────────
if [[ -d /arcline-debs ]] && ls /arcline-debs/*.deb >/dev/null 2>&1; then if [[ -d /arcline-debs ]] && ls /arcline-debs/*.deb >/dev/null 2>&1; then
log "installing Arcline toolchain packages" log "installing Arcline toolchain packages"

View File

@@ -21,8 +21,15 @@ while IFS= read -r -d '' s; do
if ! bash -n "$s"; then if ! bash -n "$s"; then
note_fail "syntax error in $s" note_fail "syntax error in $s"
fi fi
done < <(find "$ROOT/scripts" "$ROOT/btrfs" "$ROOT/toolchain" "$ROOT/tests" \ done < <(find "$ROOT/scripts" "$ROOT/btrfs" "$ROOT/toolchain" "$ROOT/tests" "$ROOT/overlays" \
-name '*.sh' -type f -print0 2>/dev/null) -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…" log "validating editions…"
for e in "${EDITIONS[@]}"; do for e in "${EDITIONS[@]}"; do