added python installer removed bash script
This commit is contained in:
@@ -2,7 +2,10 @@
|
||||
|
||||
let
|
||||
sovranSource = builtins.path { path = ../.; name = "sovran-systemsos"; };
|
||||
installer = pkgs.writeShellScriptBin "sovran-install" (builtins.readFile ./installer.sh);
|
||||
|
||||
installerPy = pkgs.writeShellScriptBin "sovran-install" ''
|
||||
exec ${pkgs.python3.withPackages (ps: [ ps.pygobject3 ])}/bin/python3 /etc/sovran/installer.py
|
||||
'';
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
@@ -13,11 +16,11 @@ in
|
||||
image.baseName = lib.mkForce "Sovran_SystemsOS";
|
||||
isoImage.splashImage = ./assets/splash-logo.png;
|
||||
|
||||
# Disable GNOME first-run tour and initial setup so the installer autostarts instead
|
||||
# Disable GNOME first-run tour and initial setup
|
||||
services.gnome.gnome-initial-setup.enable = false;
|
||||
environment.gnome.excludePackages = with pkgs; [ gnome-tour gnome-user-docs ];
|
||||
|
||||
# Allow free user to run installer commands as root without a password
|
||||
# Passwordless sudo for live ISO session
|
||||
security.sudo.wheelNeedsPassword = false;
|
||||
users.users.free = {
|
||||
isNormalUser = true;
|
||||
@@ -32,8 +35,10 @@ in
|
||||
nix-bitcoin.generateSecrets = true;
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
installer
|
||||
zenity
|
||||
installerPy
|
||||
(python3.withPackages (ps: [ ps.pygobject3 ]))
|
||||
gtk3
|
||||
gobject-introspection
|
||||
util-linux
|
||||
disko
|
||||
parted
|
||||
@@ -46,14 +51,14 @@ in
|
||||
];
|
||||
|
||||
environment.etc."sovran/logo.png".source = ./assets/splash-logo.png;
|
||||
|
||||
environment.etc."sovran/flake".source = sovranSource;
|
||||
environment.etc."sovran/installer.py".source = ./installer.py;
|
||||
|
||||
environment.etc."xdg/autostart/sovran-installer.desktop".text = ''
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Sovran Guided Installer
|
||||
Exec=bash -c "DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus ${installer}/bin/sovran-install"
|
||||
Exec=bash -c "DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus ${installerPy}/bin/sovran-install"
|
||||
Terminal=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
'';
|
||||
|
||||
600
iso/installer.py
Normal file
600
iso/installer.py
Normal file
@@ -0,0 +1,600 @@
|
||||
#!/usr/bin/env python3
|
||||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Pango, GdkPixbuf
|
||||
import subprocess
|
||||
import threading
|
||||
import os
|
||||
import sys
|
||||
|
||||
LOGO = "/etc/sovran/logo.png"
|
||||
LOG = "/tmp/sovran-install.log"
|
||||
FLAKE = "/etc/sovran/flake"
|
||||
logfile = open(LOG, "a")
|
||||
|
||||
def log(msg):
|
||||
logfile.write(msg + "\n")
|
||||
logfile.flush()
|
||||
|
||||
def run(cmd, **kwargs):
|
||||
log(f"$ {' '.join(cmd)}")
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, **kwargs)
|
||||
log(result.stdout)
|
||||
if result.returncode != 0:
|
||||
log(result.stderr)
|
||||
raise RuntimeError(result.stderr or f"Command failed: {cmd}")
|
||||
return result.stdout.strip()
|
||||
|
||||
def run_stream(cmd, text_buffer):
|
||||
log(f"$ {' '.join(cmd)}")
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
for line in proc.stdout:
|
||||
log(line.rstrip())
|
||||
GLib.idle_add(append_text, text_buffer, line)
|
||||
proc.wait()
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"Command failed with code {proc.returncode}. See {LOG}")
|
||||
|
||||
def append_text(buf, text):
|
||||
buf.insert(buf.get_end_iter(), text)
|
||||
return False
|
||||
|
||||
def human_size(nbytes):
|
||||
for unit in ["B","KB","MB","GB","TB"]:
|
||||
if nbytes < 1024:
|
||||
return f"{nbytes:.1f} {unit}"
|
||||
nbytes /= 1024
|
||||
return f"{nbytes:.1f} PB"
|
||||
|
||||
# ── Window base ────────────────────────────────────────────────────────────────
|
||||
|
||||
class InstallerWindow(Gtk.Window):
|
||||
def __init__(self):
|
||||
super().__init__(title="Sovran_SystemsOS Installer")
|
||||
self.set_default_size(800, 560)
|
||||
self.set_position(Gtk.WindowPosition.CENTER)
|
||||
self.set_resizable(False)
|
||||
if os.path.exists(LOGO):
|
||||
self.set_icon_from_file(LOGO)
|
||||
self.connect("delete-event", Gtk.main_quit)
|
||||
|
||||
self.stack = Gtk.Stack()
|
||||
self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT)
|
||||
self.stack.set_transition_duration(300)
|
||||
self.add(self.stack)
|
||||
|
||||
self.role = None
|
||||
self.boot_disk = None
|
||||
self.boot_size = None
|
||||
self.data_disk = None
|
||||
self.data_size = None
|
||||
|
||||
self.show_welcome()
|
||||
self.show_all()
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
def clear_stack(self):
|
||||
for child in self.stack.get_children():
|
||||
self.stack.remove(child)
|
||||
|
||||
def set_page(self, widget, name="page"):
|
||||
self.clear_stack()
|
||||
self.stack.add_named(widget, name)
|
||||
self.stack.set_visible_child_name(name)
|
||||
self.show_all()
|
||||
|
||||
def make_header(self, title, subtitle=None):
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||
box.set_margin_top(32)
|
||||
box.set_margin_bottom(16)
|
||||
box.set_margin_start(40)
|
||||
box.set_margin_end(40)
|
||||
|
||||
lbl = Gtk.Label()
|
||||
lbl.set_markup(f"<span font='24' weight='heavy'>{title}</span>")
|
||||
lbl.set_halign(Gtk.Align.START)
|
||||
box.pack_start(lbl, False, False, 0)
|
||||
|
||||
if subtitle:
|
||||
sub = Gtk.Label()
|
||||
sub.set_markup(f"<span font='13' foreground='#888888'>{subtitle}</span>")
|
||||
sub.set_halign(Gtk.Align.START)
|
||||
box.pack_start(sub, False, False, 0)
|
||||
|
||||
sep = Gtk.Separator()
|
||||
sep.set_margin_top(12)
|
||||
box.pack_start(sep, False, False, 0)
|
||||
return box
|
||||
|
||||
def make_nav(self, back_label=None, back_cb=None, next_label="Continue", next_cb=None):
|
||||
bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
bar.set_margin_top(8)
|
||||
bar.set_margin_bottom(20)
|
||||
bar.set_margin_start(40)
|
||||
bar.set_margin_end(40)
|
||||
|
||||
if back_label and back_cb:
|
||||
btn = Gtk.Button(label=back_label)
|
||||
btn.connect("clicked", back_cb)
|
||||
bar.pack_start(btn, False, False, 0)
|
||||
|
||||
bar.pack_start(Gtk.Label(), True, True, 0)
|
||||
|
||||
if next_cb:
|
||||
btn = Gtk.Button(label=next_label)
|
||||
btn.get_style_context().add_class("suggested-action")
|
||||
btn.connect("clicked", next_cb)
|
||||
bar.pack_end(btn, False, False, 0)
|
||||
|
||||
return bar
|
||||
|
||||
def error(self, msg):
|
||||
GLib.idle_add(self._show_error, msg)
|
||||
|
||||
def _show_error(self, msg):
|
||||
page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
page.pack_start(self.make_header("Installation Error", "Something went wrong."), False, False, 0)
|
||||
|
||||
body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
|
||||
body.set_margin_start(40)
|
||||
body.set_margin_end(40)
|
||||
|
||||
icon = Gtk.Label()
|
||||
icon.set_markup("<span font='48'>❌</span>")
|
||||
body.pack_start(icon, False, False, 0)
|
||||
|
||||
lbl = Gtk.Label(label=msg)
|
||||
lbl.set_line_wrap(True)
|
||||
lbl.set_max_width_chars(60)
|
||||
lbl.set_justify(Gtk.Justification.CENTER)
|
||||
body.pack_start(lbl, False, False, 0)
|
||||
|
||||
log_lbl = Gtk.Label()
|
||||
log_lbl.set_markup(f"<span font='11' foreground='#888'>Full log: {LOG}</span>")
|
||||
body.pack_start(log_lbl, False, False, 0)
|
||||
|
||||
page.pack_start(body, True, True, 0)
|
||||
|
||||
quit_btn = Gtk.Button(label="Close Installer")
|
||||
quit_btn.connect("clicked", Gtk.main_quit)
|
||||
nav = Gtk.Box()
|
||||
nav.set_margin_bottom(20)
|
||||
nav.set_margin_end(40)
|
||||
nav.pack_end(quit_btn, False, False, 0)
|
||||
page.pack_start(nav, False, False, 0)
|
||||
|
||||
self.set_page(page)
|
||||
return False
|
||||
|
||||
# ── Step 1: Welcome & Role ─────────────────────────────────────────────
|
||||
|
||||
def show_welcome(self):
|
||||
page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
|
||||
# Hero banner
|
||||
hero = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
hero.set_margin_top(36)
|
||||
hero.set_margin_bottom(20)
|
||||
|
||||
if os.path.exists(LOGO):
|
||||
try:
|
||||
pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(LOGO, 80, 80, True)
|
||||
img = Gtk.Image.new_from_pixbuf(pb)
|
||||
hero.pack_start(img, False, False, 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
title = Gtk.Label()
|
||||
title.set_markup("<span font='28' weight='heavy'>Sovran Systems</span>")
|
||||
hero.pack_start(title, False, False, 0)
|
||||
|
||||
sub = Gtk.Label()
|
||||
sub.set_markup("<span font='14' style='italic' foreground='#888888'>Be Digitally Sovereign</span>")
|
||||
hero.pack_start(sub, False, False, 0)
|
||||
|
||||
page.pack_start(hero, False, False, 0)
|
||||
|
||||
sep = Gtk.Separator()
|
||||
sep.set_margin_start(40)
|
||||
sep.set_margin_end(40)
|
||||
page.pack_start(sep, False, False, 0)
|
||||
|
||||
# Role selection
|
||||
lbl = Gtk.Label()
|
||||
lbl.set_markup("<span font='13'>Select your installation type:</span>")
|
||||
lbl.set_margin_top(20)
|
||||
lbl.set_margin_start(40)
|
||||
lbl.set_halign(Gtk.Align.START)
|
||||
page.pack_start(lbl, False, False, 0)
|
||||
|
||||
roles = [
|
||||
("Server + Desktop",
|
||||
"The full Sovran experience: beautiful desktop + your own cloud, secure messaging, Bitcoin node, and more.",
|
||||
"Server+Desktop"),
|
||||
("Desktop Only",
|
||||
"A beautiful, easy-to-use desktop without the background server applications.",
|
||||
"Desktop Only"),
|
||||
("Node Only",
|
||||
"Full Bitcoin node with Lightning and non-KYC buying and selling. No desktop.",
|
||||
"Node (Bitcoin-only)"),
|
||||
]
|
||||
|
||||
radio_group = None
|
||||
self._role_radios = []
|
||||
role_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||
role_box.set_margin_start(40)
|
||||
role_box.set_margin_end(40)
|
||||
role_box.set_margin_top(8)
|
||||
|
||||
for label, desc, key in roles:
|
||||
row = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
row.set_margin_bottom(8)
|
||||
|
||||
radio = Gtk.RadioButton(group=radio_group, label=label)
|
||||
radio.set_name(key)
|
||||
if radio_group is None:
|
||||
radio_group = radio
|
||||
radio.set_active(True)
|
||||
radio.get_style_context().add_class("role-radio")
|
||||
self._role_radios.append(radio)
|
||||
|
||||
desc_lbl = Gtk.Label(label=desc)
|
||||
desc_lbl.set_line_wrap(True)
|
||||
desc_lbl.set_max_width_chars(70)
|
||||
desc_lbl.set_halign(Gtk.Align.START)
|
||||
desc_lbl.set_margin_start(28)
|
||||
desc_lbl.get_style_context().add_class("dim-label")
|
||||
|
||||
row.pack_start(radio, False, False, 0)
|
||||
row.pack_start(desc_lbl, False, False, 0)
|
||||
role_box.pack_start(row, False, False, 0)
|
||||
|
||||
page.pack_start(role_box, False, False, 0)
|
||||
page.pack_start(Gtk.Label(), True, True, 0)
|
||||
page.pack_start(self.make_nav(next_label="Next →", next_cb=self.on_role_next), False, False, 0)
|
||||
self.set_page(page, "welcome")
|
||||
|
||||
def on_role_next(self, btn):
|
||||
for radio in self._role_radios:
|
||||
if radio.get_active():
|
||||
self.role = radio.get_name()
|
||||
break
|
||||
self.show_disk_confirm()
|
||||
|
||||
# ── Step 2: Disk Confirm ───────────────────────────────────────────────
|
||||
|
||||
def show_disk_confirm(self):
|
||||
# Detect disks
|
||||
try:
|
||||
raw = run(["lsblk", "-b", "-dno", "NAME,SIZE,TYPE,RO,TRAN", "-e", "7,11"])
|
||||
except Exception as e:
|
||||
self.error(str(e))
|
||||
return
|
||||
|
||||
disks = []
|
||||
for line in raw.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 4 and parts[2] == "disk" and parts[3] == "0":
|
||||
tran = parts[4] if len(parts) >= 5 else ""
|
||||
if tran != "usb":
|
||||
disks.append((parts[0], int(parts[1])))
|
||||
|
||||
if not disks:
|
||||
self.error("No valid internal drives found. USB drives are excluded.")
|
||||
return
|
||||
|
||||
disks.sort(key=lambda x: x[1])
|
||||
self.boot_disk, self.boot_size = disks[0]
|
||||
self.data_disk, self.data_size = None, None
|
||||
|
||||
BYTES_2TB = 2 * 1024 ** 4
|
||||
if len(disks) >= 2:
|
||||
d, s = disks[-1]
|
||||
if s >= BYTES_2TB:
|
||||
self.data_disk, self.data_size = d, s
|
||||
|
||||
page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
page.pack_start(self.make_header(
|
||||
"Confirm Installation",
|
||||
"Review the disks below. ALL DATA will be permanently erased."
|
||||
), False, False, 0)
|
||||
|
||||
body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
body.set_margin_start(40)
|
||||
body.set_margin_end(40)
|
||||
body.set_margin_top(8)
|
||||
|
||||
# Disk info box
|
||||
disk_frame = Gtk.Frame()
|
||||
disk_frame.set_shadow_type(Gtk.ShadowType.IN)
|
||||
disk_inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
disk_inner.set_margin_top(12)
|
||||
disk_inner.set_margin_bottom(12)
|
||||
disk_inner.set_margin_start(16)
|
||||
disk_inner.set_margin_end(16)
|
||||
|
||||
boot_lbl = Gtk.Label()
|
||||
boot_lbl.set_markup(f"<b>Boot disk:</b> /dev/{self.boot_disk} ({human_size(self.boot_size)})")
|
||||
boot_lbl.set_halign(Gtk.Align.START)
|
||||
disk_inner.pack_start(boot_lbl, False, False, 0)
|
||||
|
||||
if self.data_disk:
|
||||
data_lbl = Gtk.Label()
|
||||
data_lbl.set_markup(f"<b>Data disk:</b> /dev/{self.data_disk} ({human_size(self.data_size)})")
|
||||
data_lbl.set_halign(Gtk.Align.START)
|
||||
disk_inner.pack_start(data_lbl, False, False, 0)
|
||||
else:
|
||||
no_data = Gtk.Label()
|
||||
no_data.set_markup("<b>Data disk:</b> none detected (requires 2TB+)")
|
||||
no_data.set_halign(Gtk.Align.START)
|
||||
disk_inner.pack_start(no_data, False, False, 0)
|
||||
|
||||
disk_frame.add(disk_inner)
|
||||
body.pack_start(disk_frame, False, False, 0)
|
||||
|
||||
warn = Gtk.Label()
|
||||
warn.set_markup(
|
||||
"<span foreground='#cc0000' weight='bold'>⚠ This action cannot be undone. "
|
||||
"All existing data on the above disk(s) will be permanently destroyed.</span>"
|
||||
)
|
||||
warn.set_line_wrap(True)
|
||||
warn.set_max_width_chars(65)
|
||||
warn.set_halign(Gtk.Align.START)
|
||||
body.pack_start(warn, False, False, 0)
|
||||
|
||||
# Confirm entry
|
||||
confirm_lbl = Gtk.Label(label='Type ERASE to confirm:')
|
||||
confirm_lbl.set_halign(Gtk.Align.START)
|
||||
body.pack_start(confirm_lbl, False, False, 0)
|
||||
|
||||
self._confirm_entry = Gtk.Entry()
|
||||
self._confirm_entry.set_placeholder_text("ERASE")
|
||||
body.pack_start(self._confirm_entry, False, False, 0)
|
||||
|
||||
page.pack_start(body, False, False, 0)
|
||||
page.pack_start(Gtk.Label(), True, True, 0)
|
||||
page.pack_start(self.make_nav(
|
||||
back_label="← Back", back_cb=lambda b: self.show_welcome(),
|
||||
next_label="Begin Installation", next_cb=self.on_confirm_next
|
||||
), False, False, 0)
|
||||
self.set_page(page, "disks")
|
||||
|
||||
def on_confirm_next(self, btn):
|
||||
if self._confirm_entry.get_text().strip() != "ERASE":
|
||||
dlg = Gtk.MessageDialog(
|
||||
transient_for=self,
|
||||
modal=True,
|
||||
message_type=Gtk.MessageType.WARNING,
|
||||
buttons=Gtk.ButtonsType.OK,
|
||||
text="You must type ERASE exactly to continue."
|
||||
)
|
||||
dlg.run()
|
||||
dlg.destroy()
|
||||
return
|
||||
self.show_progress("Preparing Drives", "Partitioning and formatting your drives...", self.do_partition)
|
||||
|
||||
# ── Step 3 & 5: Progress with live log ────────────────────────────────
|
||||
|
||||
def show_progress(self, title, subtitle, worker_fn):
|
||||
page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
page.pack_start(self.make_header(title, subtitle), False, False, 0)
|
||||
|
||||
body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
body.set_margin_start(40)
|
||||
body.set_margin_end(40)
|
||||
|
||||
spinner = Gtk.Spinner()
|
||||
spinner.set_size_request(48, 48)
|
||||
spinner.start()
|
||||
body.pack_start(spinner, False, False, 0)
|
||||
|
||||
# Scrollable live log
|
||||
sw = Gtk.ScrolledWindow()
|
||||
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
sw.set_size_request(-1, 260)
|
||||
|
||||
tv = Gtk.TextView()
|
||||
tv.set_editable(False)
|
||||
tv.set_cursor_visible(False)
|
||||
tv.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
|
||||
tv.modify_font(Pango.FontDescription("Monospace 9"))
|
||||
tv.get_style_context().add_class("log-view")
|
||||
buf = tv.get_buffer()
|
||||
sw.add(tv)
|
||||
body.pack_start(sw, True, True, 0)
|
||||
|
||||
# Auto-scroll to bottom
|
||||
def on_changed(b):
|
||||
adj = sw.get_vadjustment()
|
||||
adj.set_value(adj.get_upper() - adj.get_page_size())
|
||||
buf.connect("changed", on_changed)
|
||||
|
||||
page.pack_start(body, True, True, 0)
|
||||
self.set_page(page, "progress")
|
||||
|
||||
def thread_fn():
|
||||
try:
|
||||
worker_fn(buf)
|
||||
except Exception as e:
|
||||
self.error(str(e))
|
||||
|
||||
t = threading.Thread(target=thread_fn, daemon=True)
|
||||
t.start()
|
||||
|
||||
# ── Worker: partition ─────────────────────────────────────────────────
|
||||
|
||||
def do_partition(self, buf):
|
||||
GLib.idle_add(append_text, buf, "=== Partitioning drives ===\n")
|
||||
boot_path = f"/dev/{self.boot_disk}"
|
||||
cmd = [
|
||||
"sudo", "disko", "--mode", "disko",
|
||||
f"{FLAKE}/iso/disko.nix",
|
||||
"--arg", "device", f'"{boot_path}"'
|
||||
]
|
||||
if self.data_disk:
|
||||
cmd += ["--arg", "dataDevice", f'"/dev/{self.data_disk}"']
|
||||
run_stream(cmd, buf)
|
||||
|
||||
GLib.idle_add(append_text, buf, "\n=== Generating hardware config ===\n")
|
||||
run_stream(["sudo", "nixos-generate-config", "--root", "/mnt"], buf)
|
||||
|
||||
GLib.idle_add(append_text, buf, "\n=== Copying flake to /mnt ===\n")
|
||||
run(["sudo", "cp", "/mnt/etc/nixos/hardware-configuration.nix", "/tmp/hardware-configuration.nix"])
|
||||
run(["sudo", "rm", "-rf", "/mnt/etc/nixos/"])
|
||||
run(["sudo", "mkdir", "-p", "/mnt/etc/nixos"])
|
||||
run(["sudo", "cp", "-a", f"{FLAKE}/.", "/mnt/etc/nixos/"])
|
||||
run(["sudo", "cp", "/tmp/hardware-configuration.nix", "/mnt/etc/nixos/hardware-configuration.nix"])
|
||||
|
||||
GLib.idle_add(append_text, buf, "\n=== Writing role config ===\n")
|
||||
self.write_role_state()
|
||||
|
||||
GLib.idle_add(self.show_install_step)
|
||||
|
||||
def write_role_state(self):
|
||||
is_server = str(self.role == "Server+Desktop").lower()
|
||||
is_desktop = str(self.role == "Desktop Only").lower()
|
||||
is_node = str(self.role == "Node (Bitcoin-only)").lower()
|
||||
content = f"""# THIS FILE IS AUTO-GENERATED BY THE INSTALLER. DO NOT EDIT.
|
||||
{{ config, lib, ... }}:
|
||||
{{
|
||||
sovran_systemsOS.roles.server_plus_desktop = lib.mkDefault {is_server};
|
||||
sovran_systemsOS.roles.desktop = lib.mkDefault {is_desktop};
|
||||
sovran_systemsOS.roles.node = lib.mkDefault {is_node};
|
||||
}}
|
||||
"""
|
||||
proc = subprocess.run(
|
||||
["sudo", "tee", "/mnt/etc/nixos/role-state.nix"],
|
||||
input=content, text=True, capture_output=True
|
||||
)
|
||||
log(proc.stdout)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"Failed to write role-state.nix: {proc.stderr}")
|
||||
|
||||
run(["sudo", "cp", "/mnt/etc/nixos/custom.template.nix", "/mnt/etc/nixos/custom.nix"])
|
||||
|
||||
# ── Step 4: Confirm before full install ───────────────────────────────
|
||||
|
||||
def show_install_step(self):
|
||||
page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
page.pack_start(self.make_header(
|
||||
"Drives Ready",
|
||||
"Your drives have been partitioned. Ready to install Sovran SystemsOS."
|
||||
), False, False, 0)
|
||||
|
||||
body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
body.set_margin_start(40)
|
||||
body.set_margin_end(40)
|
||||
body.set_margin_top(16)
|
||||
|
||||
icon = Gtk.Label()
|
||||
icon.set_markup("<span font='48'>✅</span>")
|
||||
body.pack_start(icon, False, False, 0)
|
||||
|
||||
info = Gtk.Label()
|
||||
info.set_markup(
|
||||
f"<b>Role:</b> {self.role}\n"
|
||||
f"<b>Boot disk:</b> /dev/{self.boot_disk} ({human_size(self.boot_size)})\n" +
|
||||
(f"<b>Data disk:</b> /dev/{self.data_disk} ({human_size(self.data_size)})"
|
||||
if self.data_disk else "<b>Data disk:</b> none")
|
||||
)
|
||||
info.set_halign(Gtk.Align.CENTER)
|
||||
info.set_justify(Gtk.Justification.CENTER)
|
||||
body.pack_start(info, False, False, 0)
|
||||
|
||||
note = Gtk.Label()
|
||||
note.set_markup(
|
||||
"<span foreground='#888'>The next step will install the full system.\n"
|
||||
"This may take <b>20–40 minutes</b> depending on your internet speed.\n"
|
||||
"Do not turn off your computer.</span>"
|
||||
)
|
||||
note.set_justify(Gtk.Justification.CENTER)
|
||||
note.set_line_wrap(True)
|
||||
body.pack_start(note, False, False, 0)
|
||||
|
||||
page.pack_start(body, True, True, 0)
|
||||
page.pack_start(self.make_nav(
|
||||
next_label="Install Now", next_cb=lambda b: self.show_progress(
|
||||
"Installing Sovran SystemsOS",
|
||||
"Building and installing your system. This will take a while...",
|
||||
self.do_install
|
||||
)
|
||||
), False, False, 0)
|
||||
self.set_page(page, "ready")
|
||||
return False
|
||||
|
||||
# ── Worker: install ───────────────────────────────────────────────────
|
||||
|
||||
def do_install(self, buf):
|
||||
GLib.idle_add(append_text, buf, "=== Running nixos-install ===\n")
|
||||
run_stream([
|
||||
"sudo", "nixos-install",
|
||||
"--root", "/mnt",
|
||||
"--flake", "/mnt/etc/nixos#nixos"
|
||||
], buf)
|
||||
GLib.idle_add(self.show_complete)
|
||||
|
||||
# ── Step 6: Complete ──────────────────────────────────────────────────
|
||||
|
||||
def show_complete(self):
|
||||
page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
page.pack_start(self.make_header("Installation Complete! 🎉", "Welcome to Sovran SystemsOS."), False, False, 0)
|
||||
|
||||
body = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
|
||||
body.set_margin_start(40)
|
||||
body.set_margin_end(40)
|
||||
body.set_margin_top(16)
|
||||
|
||||
icon = Gtk.Label()
|
||||
icon.set_markup("<span font='48'>🎉</span>")
|
||||
body.pack_start(icon, False, False, 0)
|
||||
|
||||
creds_frame = Gtk.Frame(label=" Write down your login details ")
|
||||
creds_inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
creds_inner.set_margin_top(10)
|
||||
creds_inner.set_margin_bottom(10)
|
||||
creds_inner.set_margin_start(16)
|
||||
creds_inner.set_margin_end(16)
|
||||
|
||||
user_lbl = Gtk.Label()
|
||||
user_lbl.set_markup("<b>Username:</b> free")
|
||||
user_lbl.set_halign(Gtk.Align.START)
|
||||
creds_inner.pack_start(user_lbl, False, False, 0)
|
||||
|
||||
pass_lbl = Gtk.Label()
|
||||
pass_lbl.set_markup("<b>Password:</b> free")
|
||||
pass_lbl.set_halign(Gtk.Align.START)
|
||||
creds_inner.pack_start(pass_lbl, False, False, 0)
|
||||
|
||||
creds_frame.add(creds_inner)
|
||||
body.pack_start(creds_frame, False, False, 0)
|
||||
|
||||
note = Gtk.Label()
|
||||
note.set_markup(
|
||||
"🚨 <b>Do not lose this password</b> — you will be permanently locked out if you forget it.\n\n"
|
||||
"📁 After rebooting, your system will finish setting up and save all app passwords\n"
|
||||
"(Nextcloud, Bitcoin, Matrix, etc.) to a secure PDF in your <b>Documents</b> folder."
|
||||
)
|
||||
note.set_line_wrap(True)
|
||||
note.set_max_width_chars(65)
|
||||
note.set_justify(Gtk.Justification.CENTER)
|
||||
body.pack_start(note, False, False, 0)
|
||||
|
||||
page.pack_start(body, True, True, 0)
|
||||
|
||||
reboot_btn = Gtk.Button(label="Reboot Now")
|
||||
reboot_btn.get_style_context().add_class("suggested-action")
|
||||
reboot_btn.connect("clicked", lambda b: subprocess.run(["sudo", "reboot"]))
|
||||
nav = Gtk.Box()
|
||||
nav.set_margin_bottom(20)
|
||||
nav.set_margin_end(40)
|
||||
nav.pack_end(reboot_btn, False, False, 0)
|
||||
page.pack_start(nav, False, False, 0)
|
||||
|
||||
self.set_page(page, "complete")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
win = InstallerWindow()
|
||||
Gtk.main()
|
||||
183
iso/installer.sh
183
iso/installer.sh
@@ -1,183 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
LOG=/tmp/sovran-install.log
|
||||
exec > >(tee -a "$LOG") 2>&1
|
||||
|
||||
export PATH=/run/current-system/sw/bin:$PATH
|
||||
|
||||
BYTES_2TB=$((2 * 1024 * 1024 * 1024 * 1024))
|
||||
LOGO="/etc/sovran/logo.png"
|
||||
|
||||
human_size() {
|
||||
numfmt --to=iec --suffix=B "$1"
|
||||
}
|
||||
|
||||
# ── 1. WELCOME & ROLE SELECTION ──────────────────────────────────────────
|
||||
|
||||
ROLE=$(zenity --list --radiolist \
|
||||
--icon="$LOGO" \
|
||||
--width=700 --height=450 \
|
||||
--title="Welcome to Sovran_SystemsOS Installer" \
|
||||
--text="<span font='28' weight='heavy'>Sovran Systems</span>\n<span font='14' style='italic' foreground='#aaaaaa'>Be Digitally Sovereign</span>\n\nPlease select your installation type:" \
|
||||
--print-column=2 \
|
||||
--column="Select" --column="Role" \
|
||||
TRUE "Server + Desktop — Full sovereign experience: desktop, cloud, messaging, Bitcoin node." \
|
||||
FALSE "Desktop Only — Beautiful desktop without background server applications." \
|
||||
FALSE "Node Only — Full Bitcoin node with Lightning and non-KYC buying and selling." \
|
||||
|| true)
|
||||
|
||||
if [ -z "$ROLE" ]; then
|
||||
zenity --error --icon="$LOGO" --text="Installation cancelled."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$ROLE" in
|
||||
Server*) ROLE="Server+Desktop" ;;
|
||||
Desktop*) ROLE="Desktop Only" ;;
|
||||
Node*) ROLE="Node (Bitcoin-only)" ;;
|
||||
esac
|
||||
|
||||
# ── 2. FETCH DISKS ───────────────────────────────────────────────────────
|
||||
|
||||
mapfile -t DISKS < <(lsblk -b -dno NAME,SIZE,TYPE,RO,TRAN -e 7,11 | awk '$3=="disk" && $4=="0" && $5!="usb" {print $1":"$2}')
|
||||
|
||||
if [ "${#DISKS[@]}" -eq 0 ]; then
|
||||
zenity --error --icon="$LOGO" --text="No valid internal drives found. USB drives are ignored."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS=$'\n' DISKS_SORTED=($(printf "%s\n" "${DISKS[@]}" | sort -t: -k2,2n))
|
||||
unset IFS
|
||||
|
||||
BOOT_DISK="${DISKS_SORTED[0]%%:*}"
|
||||
BOOT_SIZE="${DISKS_SORTED[0]##*:}"
|
||||
|
||||
DATA_DISK=""
|
||||
DATA_SIZE=""
|
||||
|
||||
if [ "${#DISKS_SORTED[@]}" -ge 2 ]; then
|
||||
DATA_DISK="${DISKS_SORTED[-1]%%:*}"
|
||||
DATA_SIZE="${DISKS_SORTED[-1]##*:}"
|
||||
fi
|
||||
|
||||
if [ -n "$DATA_DISK" ] && [ "$DATA_SIZE" -lt "$BYTES_2TB" ]; then
|
||||
zenity --warning --icon="$LOGO" --width=500 \
|
||||
--text="A second disk was detected (${DATA_DISK}), but it is smaller than 2TB and will not be used as a data disk."
|
||||
DATA_DISK=""
|
||||
DATA_SIZE=""
|
||||
fi
|
||||
|
||||
SUMMARY="Boot disk: /dev/${BOOT_DISK} ($(human_size "$BOOT_SIZE"))"
|
||||
if [ -n "$DATA_DISK" ]; then
|
||||
SUMMARY="${SUMMARY}\nData disk: /dev/${DATA_DISK} ($(human_size "$DATA_SIZE"))"
|
||||
else
|
||||
SUMMARY="${SUMMARY}\nData disk: none detected"
|
||||
fi
|
||||
|
||||
# ── 3. CONFIRM ERASE ─────────────────────────────────────────────────────
|
||||
|
||||
CONFIRM=$(zenity --entry \
|
||||
--icon="$LOGO" \
|
||||
--width=560 \
|
||||
--title="Confirm Installation" \
|
||||
--text="WARNING: This will permanently erase all data on:\n\n${SUMMARY}\n\nType ERASE below to confirm and begin installation.")
|
||||
|
||||
if [ "$CONFIRM" != "ERASE" ]; then
|
||||
zenity --error --icon="$LOGO" --text="Installation cancelled. Nothing was changed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BOOT_PATH="/dev/${BOOT_DISK}"
|
||||
DATA_PATH=""
|
||||
if [ -n "$DATA_DISK" ]; then
|
||||
DATA_PATH="/dev/${DATA_DISK}"
|
||||
fi
|
||||
|
||||
# ── 4. PARTITION & FORMAT ─────────────────────────────────────────────────
|
||||
|
||||
zenity --info \
|
||||
--icon="$LOGO" \
|
||||
--title="Preparing Drives" \
|
||||
--text="Please wait while your drives are being set up...\n\nThis may take a few minutes. Do not turn off your computer." \
|
||||
--width=520 &
|
||||
ZENITY_WAIT_PID=$!
|
||||
|
||||
if [ -n "$DATA_PATH" ]; then
|
||||
sudo disko --mode disko /etc/sovran/flake/iso/disko.nix \
|
||||
--arg device '"'"$BOOT_PATH"'"' \
|
||||
--arg dataDevice '"'"$DATA_PATH"'"'
|
||||
else
|
||||
sudo disko --mode disko /etc/sovran/flake/iso/disko.nix \
|
||||
--arg device '"'"$BOOT_PATH"'"'
|
||||
fi
|
||||
|
||||
kill $ZENITY_WAIT_PID 2>/dev/null || true
|
||||
|
||||
# ── 5. COPY CONFIG ────────────────────────────────────────────────────────
|
||||
|
||||
sudo nixos-generate-config --root /mnt
|
||||
|
||||
cp /mnt/etc/nixos/hardware-configuration.nix /tmp/hardware-configuration.nix
|
||||
sudo rm -rf /mnt/etc/nixos/*
|
||||
sudo cp -a /etc/sovran/flake/* /mnt/etc/nixos/
|
||||
sudo cp /tmp/hardware-configuration.nix /mnt/etc/nixos/hardware-configuration.nix
|
||||
|
||||
# ── 6. APPLY ROLE STATE & TEMPLATE ───────────────────────────────────────
|
||||
|
||||
IS_SERVER="false"
|
||||
IS_DESKTOP="false"
|
||||
IS_NODE="false"
|
||||
|
||||
case "$ROLE" in
|
||||
"Server+Desktop") IS_SERVER="true" ;;
|
||||
"Desktop Only") IS_DESKTOP="true" ;;
|
||||
"Node (Bitcoin-only)") IS_NODE="true" ;;
|
||||
esac
|
||||
|
||||
sudo tee /mnt/etc/nixos/role-state.nix > /dev/null <<EOF
|
||||
# THIS FILE IS AUTO-GENERATED BY THE INSTALLER. DO NOT EDIT.
|
||||
# To change your role later, edit custom.nix instead.
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
sovran_systemsOS.roles.server_plus_desktop = lib.mkDefault ${IS_SERVER};
|
||||
sovran_systemsOS.roles.desktop = lib.mkDefault ${IS_DESKTOP};
|
||||
sovran_systemsOS.roles.node = lib.mkDefault ${IS_NODE};
|
||||
}
|
||||
EOF
|
||||
|
||||
sudo cp /mnt/etc/nixos/custom.template.nix /mnt/etc/nixos/custom.nix
|
||||
|
||||
# ── 7. VERIFY FILES ───────────────────────────────────────────────────────
|
||||
|
||||
for f in /mnt/etc/nixos/role-state.nix /mnt/etc/nixos/custom.nix; do
|
||||
if [ ! -f "$f" ]; then
|
||||
zenity --error --icon="$LOGO" --width=500 \
|
||||
--title="Installation Error" \
|
||||
--text="A required file is missing:\n\n${f}\n\nInstallation cannot continue. Please check the log at ${LOG} and try again."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# ── 8. FINAL INSTALL ─────────────────────────────────────────────────────
|
||||
|
||||
zenity --info \
|
||||
--icon="$LOGO" \
|
||||
--title="Installing Sovran SystemsOS" \
|
||||
--text="Please wait while your system is being installed...\n\nThis may take 20-40 minutes depending on your internet speed.\nDo not turn off your computer." \
|
||||
--width=520 &
|
||||
ZENITY_INSTALL_PID=$!
|
||||
|
||||
sudo nixos-install --root /mnt --flake /mnt/etc/nixos#nixos
|
||||
|
||||
kill $ZENITY_INSTALL_PID 2>/dev/null || true
|
||||
|
||||
# ── 9. COMPLETE ───────────────────────────────────────────────────────────
|
||||
|
||||
zenity --info \
|
||||
--icon="$LOGO" \
|
||||
--width=600 \
|
||||
--title="Installation Complete!" \
|
||||
--text="Installation Successful!\n\nPlease write down your login details before rebooting:\n\nUsername: free\nPassword: free\n\nCRITICAL: Do not lose this password or you will be permanently locked out.\n\nAfter rebooting your system will finish setting up and save all app passwords (Nextcloud, Bitcoin, Matrix, etc.) to a secure PDF in your Documents folder.\n\nClick OK to reboot into your new system!"
|
||||
|
||||
sudo reboot
|
||||
Reference in New Issue
Block a user