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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
71
installer/README.md
Normal 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
260
installer/arcline-installer
Executable 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())
|
||||||
16
overlays/live/etc/systemd/system/arcline-installer.service
Normal file
16
overlays/live/etc/systemd/system/arcline-installer.service
Normal 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
|
||||||
11
overlays/live/usr/local/sbin/arcline-installer-session
Executable file
11
overlays/live/usr/local/sbin/arcline-installer-session
Executable 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
|
||||||
@@ -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
97
scripts/build-live.sh
Executable 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"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user