Compare commits

..

85 Commits

Author SHA1 Message Date
ada17eefd7 Switch to systemd-boot for UEFI while keeping GRUB2 for BIOS/MBR 2026-02-10 13:29:02 +01:00
2e8e99f481 Add --force to UEFI grub2-install to bypass Fedora Secure Boot warning 2026-02-10 13:26:49 +01:00
53227e0f8e Fix UEFI grub2-install: add missing modules package and fix DNF stderr logging 2026-02-09 19:05:27 +01:00
2762f2f767 Robust log capture on SIGINT/SIGTERM and cleaner Discord reporting 2026-02-09 18:39:17 +01:00
19254e128c Enhance Discord error reporting: code blocks and crash robustness 2026-02-09 18:00:20 +01:00
5abba6ce88 Improve Discord logging for errors and further fix UEFI mount robustness 2026-02-09 17:41:12 +01:00
0ec7de3937 Fix UEFI GRUB installation: ensure ESP is mounted to /boot/efi and improve error logging 2026-02-09 16:53:30 +01:00
cbc1b05eca Simplify run_vm_bios.sh to unconditionally load clean-state snapshot 2026-02-09 15:24:46 +01:00
e8afc31d00 Enable snapshot support in run_vm_bios.sh with automatic detection 2026-02-09 15:14:09 +01:00
59219130bd Fix run_vm_bios.sh: add missing backslashes and improve CDROM boot compatibility 2026-02-09 15:08:23 +01:00
e8a2f7aaa9 Redo backend installer logic to support BIOS/MBR boot on older devices and switch to GRUB2 2026-02-09 15:02:14 +01:00
b37fc8d060 Update run_vm_uefi.sh 2026-02-09 14:22:45 +01:00
44af152887 Revert "Improve offline repository discovery and dnf configuration for ISO installs"
This reverts commit 297f2cd3c2.
2026-02-05 19:43:12 +01:00
5238fd29d9 Revert "Fix dnf failure by adding /run mount, removing --cacheonly, and ensuring host-config usage"
This reverts commit 951a1b7fdc.
2026-02-05 19:43:12 +01:00
74fedf9001 Revert "Further improvements to dnf robustness and repo discovery for offline installs"
This reverts commit 5df00a5814.
2026-02-05 19:43:12 +01:00
5df00a5814 Further improvements to dnf robustness and repo discovery for offline installs 2026-02-05 19:39:54 +01:00
951a1b7fdc Fix dnf failure by adding /run mount, removing --cacheonly, and ensuring host-config usage 2026-02-05 19:32:56 +01:00
297f2cd3c2 Improve offline repository discovery and dnf configuration for ISO installs 2026-02-05 19:28:41 +01:00
ce2c242454 revert 4278a3198d
revert Add minimal-linux-installer skill with proven installation patterns
2026-02-05 18:32:01 +01:00
4278a3198d Add minimal-linux-installer skill with proven installation patterns 2026-02-05 18:30:55 +01:00
943cfee8bb IT ACTUALLY INSTALLS 2026-02-05 18:16:08 +01:00
9cfd6ad2e0 Use robust usermod -p method for setting passwords 2026-02-05 17:54:14 +01:00
23b5f017d4 Fix password setting using chpasswd -R and improve screen wake lock 2026-02-05 17:39:01 +01:00
ee83411333 Fix password setting by using passwd --stdin and sync 2026-02-05 17:25:00 +01:00
7613bdf8d5 Fix password setting and inhibit system sleep during install 2026-02-05 17:03:36 +01:00
400235067d Fix bootctl unrecognized option and improve repo discovery 2026-02-05 16:46:59 +01:00
f3e7122d02 Fix dnf5 deferred transaction and add shadow-utils for user creation 2026-02-05 16:27:57 +01:00
afe18c1eee Fix dnf error by removing localpkg_only and improve repo search 2026-02-05 15:40:42 +01:00
76c638de8e Improve offline installation by searching for repo in multiple paths 2026-02-05 15:37:18 +01:00
1238dcdabd Remove accidental log file 2026-02-05 15:17:53 +01:00
f7fc354f3f Fix UEFI boot by mounting efivarfs and forcing systemd-boot install 2026-02-05 15:17:48 +01:00
3a226108ec Fix: ensure disk is unmounted before partitioning and use sudo for reboot 2026-02-05 14:55:18 +01:00
45b0fa5a84 . 2026-02-05 14:54:15 +01:00
6b7c7debff Increase VM memory allocation to 8GB 2026-02-05 14:49:59 +01:00
9fcbf6f204 Add automatic dependency installation to run.sh 2026-02-05 14:34:35 +01:00
e5f46324e4 Fix UEFI snapshot support and ensure sudo privileges 2026-02-05 14:23:44 +01:00
4eff9d7e22 Update UEFI VM script with correct paths and add snapshot script 2026-02-05 14:18:39 +01:00
7bfab2ac5b Update window.py 2026-02-05 14:00:49 +01:00
8bac02357c Send logs as file, create sudoer user, and improve bootloader reliability 2026-02-05 13:22:34 +01:00
bba28a6f77 Remove accidental log file 2026-02-05 12:59:54 +01:00
be674e89e0 Switch to systemd-boot, full log sending, and offline install fix 2026-02-05 12:59:24 +01:00
e99290eebe . 2026-02-05 12:48:36 +01:00
eb03edb050 . 2026-02-05 12:39:41 +01:00
f7bebc7f88 Add network logging to Discord webhook for testing branch
- Add network_logging.py module with Discord webhook integration
- Log session start with unique session ID and mode (MOCK/PRODUCTION)
- Log all page navigations when clicking Next
- Log all user selections (disk, install mode, modules, user info)
- Log installation progress through each step
- Send install completion/failure logs
- Auto-flush logs every 2 seconds and immediately on critical events

TESTING BRANCH ONLY - Remove before merging to main
2026-02-04 21:18:18 +01:00
e611f174be Update os_install.py 2026-02-03 21:32:51 +01:00
b70ff549a7 Update partitioning.py 2026-02-03 21:31:17 +01:00
bb31814233 Update partitioning.py 2026-02-03 21:30:27 +01:00
00ba6e5c89 feat: verbose installation logs, improved auto-partitioning logic, and UI tweaks 2026-02-03 21:24:31 +01:00
7d43b82ce1 fix(ui): reorder partition dialog, fix mount point selection, add threaded install with logs 2026-02-03 21:06:16 +01:00
af86d357a4 fix(ui): correct mount point selection pre-fill and simplify partition creation dialog 2026-02-03 20:57:29 +01:00
848b2e7e74 fix: mount pseudo-fs during install, simplify partitioning, and add mount point config 2026-02-03 20:49:41 +01:00
dc417d15d3 fix(backend): use host config for dnf install to resolve repos 2026-02-03 20:29:03 +01:00
7a8e8ccbed fix(partitioning): avoid alignment errors by using fill-gap logic for full space 2026-02-03 20:19:25 +01:00
7371da2451 fix(backend): improve partition creation robustness and formatting 2026-02-03 20:03:24 +01:00
9f5bade34c feat(partitioning): support fat32 and add validation for system partition name 2026-02-03 19:53:51 +01:00
11fca148be fix(ui): handle NoneType fstype and remove duplicate code in partitioning page 2026-02-03 19:49:52 +01:00
b7a049d86f fix(ui): correct Gtk.Button instantiation in partitioning page 2026-02-03 18:13:04 +01:00
2676da751b Fix PartitioningPage being stuck on detecting due to duplicate function 2026-02-03 17:23:19 +01:00
22e1fa8f62 Implement manual partition management (create/delete/mountpoints) 2026-02-03 17:07:45 +01:00
efe25f3e11 Ensure disk device path has /dev/ prefix 2026-02-03 17:01:36 +01:00
2f9338af0a Fix logic error in page transitions 2026-02-03 16:43:19 +01:00
33d989cfad Hook up GUI Install button to backend logic 2026-02-03 16:34:36 +01:00
2377b0269a Implement minimal OS and Bootloader installation logic 2026-02-03 16:28:55 +01:00
3c4870b104 Implement basic partitioning backend and CLI interface 2026-02-03 16:20:59 +01:00
aae235e81f Format icons SVG files for better readability 2026-02-03 12:03:25 +01:00
759b4c4007 Use the icon 2026-02-03 01:26:30 +01:00
9958b9a65b Create org.iridium.Installer-symbolic.svg 2026-02-03 01:26:25 +01:00
1afdf99a6f Temporary (or probably permanent, you know how this works) icon 2026-02-03 01:17:48 +01:00
26757288c4 Add mock mode support and input validation 2026-02-02 20:19:36 +01:00
7a69d62782 Module titles 2026-02-02 20:15:40 +01:00
f691be219d Summary page 2026-02-02 20:05:11 +01:00
281f7488af Update README.md 2026-02-02 20:03:40 +01:00
931f701591 Center content vertically in user and welcome pages 2026-02-02 15:26:03 +01:00
52897a1dd0 Why the fuck was it in that order 2026-02-02 15:24:40 +01:00
2c7b9c5b1f Automatic usernames 2026-02-02 15:20:23 +01:00
658040c138 Better - force selection, gray out button 2026-02-02 15:08:03 +01:00
7c899b8e86 Can't continue without selecting disk 2026-02-02 15:03:30 +01:00
a098f20a38 English as the only language option 2026-02-02 14:59:35 +01:00
3bff26928a Create additional_modules.py 2026-02-02 14:59:20 +01:00
001d07cc80 Update warning label to use WARNING instead of WARN 2026-02-02 12:22:13 +01:00
ae16c2ca23 Update README.md 2026-02-02 12:20:30 +01:00
0f448bfa6c Automatic partition sizes 2026-02-02 12:17:13 +01:00
f341fe0d60 Windows 8-aah welcome message 2026-02-02 12:03:06 +01:00
f642d7ef94 No toolbar, like a proper adwaita app should 2026-02-02 12:00:57 +01:00
f8b9cb62f9 Proper automatic/manual partitioning switch 2026-02-02 11:50:44 +01:00
21 changed files with 2244 additions and 106 deletions

View File

@@ -0,0 +1,17 @@
<div align="center">
<img src="data/icons/org.iridium.Installer.svg" alt="Iridium OS Installer Icon" width="128" height="128">
# Iridium OS Installer
</div>
> [!WARNING]
> For now this is only a mockup and not actually functional.
## What is Iridium Installer?
Iridium Installer is a modern, GTK4/Libadwaita based installer designed for Iridium OS. It aims to provide a simple and elegant installation experience.
## Features
- Modern UI using **GTK4** and **Libadwaita**
- Automatic and Manual partitioning modes
- User configuration
- Module selection

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" ?>
<svg
height="16px"
viewBox="0 0 16 16"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M 8 1 C 4.132812 1 1 4.132812 1 8 C 1 11.867188 4.132812 15 8 15 C 11.867188 15 15 11.867188 15 8 C 15 4.132812 11.867188 1 8 1 Z M 8 3 C 10.761719 3 13 5.238281 13 8 C 13 10.761719 10.761719 13 8 13 C 5.238281 13 3 10.761719 3 8 C 3 5.238281 5.238281 3 8 3 Z M 8 3"
fill="#2e3436"
/>
<path
d="M 8 5 C 6.34375 5 5 6.34375 5 8 C 5 9.65625 6.34375 11 8 11 C 9.65625 11 11 9.65625 11 8 C 11 6.34375 9.65625 5 8 5 Z M 8 6 C 9.105469 6 10 6.894531 10 8 C 10 9.105469 9.105469 10 8 10 C 6.894531 10 6 9.105469 6 8 C 6 6.894531 6.894531 6 8 6 Z M 8 6"
fill="#2e3436"
/>
</svg>

After

Width:  |  Height:  |  Size: 772 B

View File

@@ -0,0 +1,39 @@
<svg
width="128"
height="128"
viewBox="0 0 128 128"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient id="circle-bg" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#5E5C64" />
<stop offset="100%" stop-color="#3D3846" />
</linearGradient>
</defs>
<circle cx="64" cy="64" r="56" fill="url(#circle-bg)" />
<circle
cx="64"
cy="64"
r="40"
fill="none"
stroke="#F6D32D"
stroke-width="8"
stroke-dasharray="75 25"
stroke-linecap="round"
opacity="0.9"
/>
<circle
cx="64"
cy="64"
r="28"
fill="none"
stroke="#F5C211"
stroke-width="4"
stroke-dasharray="45 20"
stroke-linecap="round"
opacity="0.8"
/>
<circle cx="64" cy="64" r="10" fill="#F6D32D" />
</svg>

After

Width:  |  Height:  |  Size: 871 B

View File

@@ -0,0 +1,9 @@
[Desktop Entry]
Name=Iridium Installer
Comment=Install Iridium OS on your computer
Exec=iridium-installer
Icon=org.iridium.Installer
Type=Application
Categories=System;Settings;
StartupNotify=true
Terminal=false

View File

View File

@@ -0,0 +1,327 @@
import subprocess
import logging
import os
logger = logging.getLogger(__name__)
# Import network logging for critical disk operations
from .network_logging import log_to_discord
def log_disk_operation(operation, status="start", details=""):
log_to_discord("INFO", f"DISK_{operation}_{status}: {details}", module="disk")
class CommandResult:
def __init__(self, stdout, stderr, returncode):
self.stdout = stdout
self.stderr = stderr
self.returncode = returncode
def run_command(cmd, check=True):
logger.info(f"Running command: {' '.join(cmd)}")
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1
)
stdout_lines = []
stderr_lines = []
def read_stream(stream, line_list, log_level):
for line in stream:
line_clean = line.strip()
if line_clean:
log_level(line_clean)
line_list.append(line)
import threading
t1 = threading.Thread(
target=read_stream, args=(process.stdout, stdout_lines, logger.info)
)
t2 = threading.Thread(
target=read_stream, args=(process.stderr, stderr_lines, logger.error)
)
t1.start()
t2.start()
t1.join()
t2.join()
returncode = process.wait()
stdout_str = "".join(stdout_lines)
stderr_str = "".join(stderr_lines)
if check and returncode != 0:
raise subprocess.CalledProcessError(
returncode, cmd, output=stdout_str, stderr=stderr_str
)
return CommandResult(stdout_str, stderr_str, returncode)
def get_partition_device(disk_device, partition_number):
"""
Returns the partition device path.
Handles NVMe style (p1) vs sd style (1).
"""
if disk_device[-1].isdigit():
return f"{disk_device}p{partition_number}"
return f"{disk_device}{partition_number}"
def get_total_memory():
"""Returns total system memory in bytes."""
try:
return os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
except (ValueError, OSError):
with open("/proc/meminfo", "r") as f:
for line in f:
if line.startswith("MemTotal:"):
parts = line.split()
return int(parts[1]) * 1024
return 0
def get_disk_size(disk_device):
"""Returns disk size in bytes using blockdev."""
try:
res = run_command(["blockdev", "--getsize64", disk_device])
return int(res.stdout.strip())
except Exception as e:
logger.error(f"Failed to get disk size: {e}")
return 0
def ensure_unmounted(disk_device):
"""
Finds all mounted partitions belonging to disk_device and unmounts them.
"""
logger.info(f"Ensuring all partitions on {disk_device} are unmounted...")
try:
# Get all partitions for this disk from lsblk
res = run_command(["lsblk", "-nlo", "NAME,MOUNTPOINT", disk_device])
for line in res.stdout.splitlines():
parts = line.split()
if len(parts) >= 2:
# NAME is usually the leaf name (e.g. vda1), we need full path
# But lsblk -lp gives full paths
pass
# Simpler approach: findmnt or just look at /proc/mounts
# Let's use lsblk -lp to get full paths
res = run_command(["lsblk", "-lpno", "NAME,MOUNTPOINT", disk_device])
for line in res.stdout.splitlines():
# line looks like "/dev/vda1 /mnt/boot" or "/dev/vda1"
parts = line.split()
if len(parts) >= 2:
dev_path = parts[0]
mount_point = parts[1]
if mount_point and mount_point != "None":
logger.info(f"Unmounting {dev_path} from {mount_point}...")
run_command(["umount", "-l", dev_path])
except Exception as e:
logger.warning(f"Error during pre-partition unmount: {e}")
def is_uefi():
"""Checks if the system is booted in UEFI mode."""
return os.path.exists("/sys/firmware/efi")
def auto_partition_disk(disk_device):
"""
Automatically partitions the disk based on boot mode:
UEFI (GPT): ESP (1GB), Root (Remaining), Swap (RAM+2GB)
BIOS (MBR): Root (Remaining, Bootable), Swap (RAM+2GB)
"""
uefi = is_uefi()
logger.info(f"Starting auto-partitioning on {disk_device} (UEFI: {uefi})")
log_disk_operation("PARTITION", "start", f"Disk: {disk_device}, UEFI: {uefi}")
ensure_unmounted(disk_device)
disk_size = get_disk_size(disk_device)
ram_size = get_total_memory()
# Calculate sizes
swap_mb = int((ram_size / (1024 * 1024)) + 2048)
# Cap swap at 16GB for sanity on large RAM systems unless disk is huge
if disk_size < 100 * 1024**3: # < 100GB
swap_mb = min(swap_mb, 4096)
disk_mb = disk_size / (1024 * 1024)
use_swap = disk_mb > (10240 + swap_mb) # Only use swap if we have at least 10GB for root
if uefi:
# GPT Layout
logger.info("Using GPT layout for UEFI")
run_command(["sgdisk", "-Z", disk_device])
run_command(["sgdisk", "-o", disk_device])
# 1. ESP (1GB)
run_command(["sgdisk", "-n", "1:0:+1024M", "-t", "1:ef00", "-c", "1:EFI System", disk_device])
# 2. Swap (if enabled)
if use_swap:
run_command(["sgdisk", "-n", f"3:-{swap_mb}M:0", "-t", "3:8200", "-c", "3:Swap", disk_device])
# 3. Root
run_command(["sgdisk", "-n", "2:0:0", "-t", "2:8300", "-c", "2:Root", disk_device])
else:
# MBR Layout for BIOS
logger.info("Using MBR layout for BIOS")
# Wipe disk using dd to be sure MBR is cleared
run_command(["dd", "if=/dev/zero", f"of={disk_device}", "bs=512", "count=100"])
# Use parted for MBR
run_command(["parted", "-s", disk_device, "mklabel", "msdos"])
if use_swap:
# Root first, then swap at end
root_end = int(disk_mb - swap_mb)
run_command(["parted", "-s", disk_device, "mkpart", "primary", "ext4", "1MiB", f"{root_end}MiB"])
run_command(["parted", "-s", disk_device, "mkpart", "primary", "linux-swap", f"{root_end}MiB", "100%"])
run_command(["parted", "-s", disk_device, "set", "1", "boot", "on"])
else:
run_command(["parted", "-s", disk_device, "mkpart", "primary", "ext4", "1MiB", "100%"])
run_command(["parted", "-s", disk_device, "set", "1", "boot", "on"])
run_command(["partprobe", disk_device])
import time
time.sleep(2)
# Identify partitions
if uefi:
efi_part = get_partition_device(disk_device, 1)
root_part = get_partition_device(disk_device, 2)
swap_part = get_partition_device(disk_device, 3) if use_swap else None
else:
efi_part = None # BIOS doesn't use ESP
root_part = get_partition_device(disk_device, 1)
swap_part = get_partition_device(disk_device, 2) if use_swap else None
# Format
if uefi:
logger.info(f"Formatting EFI partition {efi_part}...")
run_command(["mkfs.vfat", "-F32", efi_part])
if use_swap:
logger.info(f"Formatting Swap partition {swap_part}...")
run_command(["mkswap", swap_part])
logger.info(f"Formatting Root partition {root_part}...")
run_command(["mkfs.ext4", "-F", root_part])
result = {"efi": efi_part, "root": root_part, "swap": swap_part}
log_disk_operation("PARTITION", "complete", str(result))
return result
def mount_partitions(partition_info, mount_root="/mnt"):
"""
Mounts the partitions into mount_root.
"""
import os
log_disk_operation("MOUNT", "start", f"Mounting to {mount_root}")
# 1. Mount Root
if not os.path.exists(mount_root):
os.makedirs(mount_root)
run_command(["mount", partition_info["root"], mount_root])
# 2. Mount EFI (if exists)
if partition_info.get("efi"):
# For systemd-boot, we prefer mounting ESP to /boot
efi_mount = os.path.join(mount_root, "boot")
os.makedirs(efi_mount, exist_ok=True)
run_command(["mount", partition_info["efi"], efi_mount])
else:
# For BIOS, /boot is just a directory on root or we could make a separate /boot
# Current logic keeps it on root.
pass
# 3. Enable Swap
if partition_info.get("swap"):
run_command(["swapon", partition_info["swap"]])
logger.info(f"Partitions mounted at {mount_root}")
log_disk_operation("MOUNT", "complete", f"Root: {partition_info['root']}")
def create_partition(
disk_device, size_mb, type_code="8300", name="Linux filesystem", fstype=None
):
"""
Creates a new partition on the disk and formats it.
type_code: ef00 (EFI), 8200 (Swap), 8300 (Linux)
fstype: ext4, fat32, swap
"""
# Find next available partition number
res = run_command(["sgdisk", "-p", disk_device])
# Very basic parsing to find the next number
existing_nums = []
for line in res.stdout.splitlines():
parts = line.split()
if parts and parts[0].isdigit():
existing_nums.append(int(parts[0]))
next_num = 1
while next_num in existing_nums:
next_num += 1
# Use -g (mbrtogpt) to ensure we can work on the disk if it was MBR
# size_mb=0 means use all available space in the gap
size_spec = f"+{size_mb}M" if size_mb > 0 else "0"
run_command(
[
"sgdisk",
"-g",
"-n",
f"{next_num}:0:{size_spec}",
"-t",
f"{next_num}:{type_code}",
"-c",
f"{next_num}:{name}",
disk_device,
]
)
run_command(["partprobe", disk_device])
# Wait for partition node to appear
try:
run_command(["udevadm", "settle", "--timeout=5"])
except Exception:
import time
time.sleep(2)
part_dev = get_partition_device(disk_device, next_num)
if fstype == "fat32":
run_command(["mkfs.vfat", "-F32", part_dev])
elif fstype == "ext4":
run_command(["mkfs.ext4", "-F", part_dev])
elif fstype == "swap":
run_command(["mkswap", part_dev])
return next_num
def delete_partition(disk_device, part_num):
"""Deletes a partition by number."""
run_command(["sgdisk", "-d", str(part_num), disk_device])
run_command(["partprobe", disk_device])
def wipe_disk(disk_device):
"""Zaps the disk and creates a new GPT table."""
run_command(["sgdisk", "-Z", disk_device])
run_command(["sgdisk", "-o", disk_device])
run_command(["partprobe", disk_device])

View File

@@ -0,0 +1,143 @@
import logging
import requests
import threading
import time
import os
import atexit
import sys
import signal
from datetime import datetime
logger = logging.getLogger(__name__ + ".network_logging")
DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1468696228647932280/L9XSHS6TPEeK0wwJTFdK9RUyZvztSGQBd4xEfVvb4Y1AXGQAOc4YTsuxeFuWC9HxymJn"
FULL_LOG = []
QUEUE_LOCK = threading.Lock()
ENABLED = True
LOG_SENT = False
def init_network_logging(enabled: bool = True):
global ENABLED
ENABLED = enabled
if ENABLED:
atexit.register(send_full_log)
# Handle signals to ensure atexit runs
def signal_handler(sig, frame):
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
def log_to_discord(level: str, message: str, module: str = "general"):
if not ENABLED:
return
timestamp = datetime.utcnow().isoformat()
log_entry = {
"timestamp": timestamp,
"level": level.upper(),
"module": module,
"message": message,
}
with QUEUE_LOCK:
FULL_LOG.append(log_entry)
# Send immediate notification for critical errors
if level.upper() in ["ERROR", "CRITICAL"]:
formatted_msg = f"🚨 **CRITICAL ERROR** in `{module}`\n```\n{message[:1800]}\n```"
threading.Thread(
target=send_discord_message,
args=(formatted_msg,),
daemon=False
).start()
def flush_logs():
pass
def send_full_log():
"""Sends the entire session log as a text file attachment at the end."""
global LOG_SENT
if not ENABLED or LOG_SENT:
return
LOG_SENT = True
# Final sync wait
time.sleep(1)
with QUEUE_LOCK:
logs_to_send = FULL_LOG.copy()
if not logs_to_send:
return
temp_file = f"/tmp/iridium_install_log_{int(time.time())}.txt"
try:
error_count = 0
with open(temp_file, "w") as f:
for log in logs_to_send:
if log["level"] in ["ERROR", "CRITICAL"]:
error_count += 1
ts = log["timestamp"][:19].replace("T", " ")
line = f"[{ts}] [{log['level']}] [{log['module']}] {log['message']}\n"
f.write(line)
with open(temp_file, "rb") as f:
files = {"file": ("iridium_install_log.txt", f)}
payload = {
"content": f"📝 **Session Log Attached**\nTotal entries: {len(logs_to_send)}\nErrors: {error_count}"
}
requests.post(DISCORD_WEBHOOK_URL, data=payload, files=files, timeout=30)
except Exception as e:
print(f"Failed to send full log file to Discord: {e}")
finally:
if os.path.exists(temp_file):
try:
os.remove(temp_file)
except: pass
def send_discord_message(content: str):
try:
payload = {"content": content}
requests.post(DISCORD_WEBHOOK_URL, json=payload, timeout=5)
except Exception as e:
print(f"Discord webhook error: {e}")
class DiscordLogHandler(logging.Handler):
def __init__(self, module_name: str = "general"):
super().__init__()
self.module_name = module_name
def emit(self, record: logging.LogRecord):
if record.levelno < logging.INFO:
return
level_map = {
logging.INFO: "INFO",
logging.WARNING: "WARN",
logging.ERROR: "ERROR",
logging.CRITICAL: "CRITICAL",
}
level = level_map.get(record.levelno, "INFO")
message = self.format(record)
module_name = (
self.module_name
if self.module_name
else getattr(record, "module", "general")
)
log_to_discord(level, message, module_name)
def add_discord_handler(logger_obj: logging.Logger, module: str = None):
module_name = (
module if module else (logger_obj.name if logger_obj.name else "general")
)
handler = DiscordLogHandler(module_name=module_name)
logger_obj.addHandler(handler)

View File

@@ -0,0 +1,332 @@
import logging
import os
import subprocess
import sys
from contextlib import contextmanager
logger = logging.getLogger(__name__)
# Import network logging for critical operations
from .network_logging import log_to_discord
def log_os_install(operation, status="start", details=""):
log_to_discord(
"INFO", f"OSINSTALL_{operation}_{status}: {details}", module="os_install"
)
class CommandResult:
def __init__(self, stdout, stderr, returncode):
self.stdout = stdout
self.stderr = stderr
self.returncode = returncode
def run_command(cmd, check=True):
logger.info(f"Running command: {' '.join(cmd)}")
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1, # Line buffered
)
stdout_lines = []
stderr_lines = []
# Helper to read stream
def read_stream(stream, line_list, log_level):
for line in stream:
line_clean = line.strip()
if line_clean:
log_level(line_clean)
line_list.append(line)
import threading
t1 = threading.Thread(
target=read_stream, args=(process.stdout, stdout_lines, logger.info)
)
# Log stderr as INFO to avoid Discord notification spam, but still capture it
t2 = threading.Thread(
target=read_stream, args=(process.stderr, stderr_lines, logger.info)
)
t1.start()
t2.start()
t1.join()
t2.join()
returncode = process.wait()
stdout_str = "".join(stdout_lines)
stderr_str = "".join(stderr_lines)
if check and returncode != 0:
error_msg = f"Command failed: {' '.join(cmd)}\nExit Code: {returncode}\nStderr: {stderr_str}"
# Log this specific failure as ERROR so it DOES go to Discord
logger.error(error_msg)
raise subprocess.CalledProcessError(
returncode, cmd, output=stdout_str, stderr=stderr_str
)
return CommandResult(stdout_str, stderr_str, returncode)
@contextmanager
def mount_pseudo_fs(mount_root):
"""
Context manager to bind mount /dev, /proc, /sys, and efivarfs into mount_root.
"""
logger.info(f"Mounting pseudo-filesystems to {mount_root}...")
mounts = ["dev", "proc", "sys"]
mounted_paths = []
try:
for fs in mounts:
target = os.path.join(mount_root, fs)
os.makedirs(target, exist_ok=True)
run_command(["mount", "--bind", f"/{fs}", target])
mounted_paths.append(target)
# Mount efivarfs if it exists on the host
efivars_path = "/sys/firmware/efi/efivars"
if os.path.exists(efivars_path):
target = os.path.join(mount_root, "sys/firmware/efi/efivars")
os.makedirs(target, exist_ok=True)
try:
# Try bind mount first
run_command(["mount", "--bind", efivars_path, target])
mounted_paths.append(target)
except Exception:
try:
# Fallback to direct mount
run_command(["mount", "-t", "efivarfs", "efivarfs", target])
mounted_paths.append(target)
except Exception as e:
logger.warning(f"Failed to mount efivarfs: {e}")
yield
finally:
logger.info(f"Unmounting pseudo-filesystems from {mount_root}...")
for path in reversed(mounted_paths):
try:
run_command(["umount", "-l", path])
except Exception as e:
logger.warning(f"Failed to unmount {path}: {e}")
def is_uefi():
return os.path.exists("/sys/firmware/efi")
def install_minimal_os(mount_root, releasever="43"):
"""
Installs minimal Fedora packages to mount_root.
"""
logger.info(f"Installing minimal Fedora {releasever} to {mount_root}...")
log_os_install("INSTALL", "start", f"Target: {mount_root}, Release: {releasever}")
uefi = is_uefi()
packages = [
"basesystem",
"bash",
"coreutils",
"kernel",
"systemd",
"dnf",
"shadow-utils",
"util-linux",
"passwd",
"rootfiles",
"vim-minimal",
]
if uefi:
packages += ["systemd-boot-unsigned", "efibootmgr"]
else:
packages += ["grub2-pc", "grub2-tools", "grubby"]
# Offline installation logic
possible_repos = [
"/run/install/repo",
"/run/install/source",
"/mnt/install/repo",
"/run/initramfs/live",
"/run/initramfs/isoscan",
]
iso_repo = None
for path in possible_repos:
if os.path.exists(os.path.join(path, "repodata")):
iso_repo = path
break
elif os.path.exists(os.path.join(path, "Packages")):
iso_repo = path
break
# Try searching in /run/media if not found
if not iso_repo and os.path.exists("/run/media"):
try:
for user in os.listdir("/run/media"):
user_path = os.path.join("/run/media", user)
if os.path.isdir(user_path):
for label in os.listdir(user_path):
label_path = os.path.join(user_path, label)
if os.path.exists(os.path.join(label_path, "repodata")):
iso_repo = label_path
break
if iso_repo: break
except Exception: pass
dnf_args = []
if iso_repo:
logger.info(f"Found ISO repository at {iso_repo}. Using strictly offline mode.")
dnf_args = [
"--disablerepo=*",
f"--repofrompath=iridium-iso,{iso_repo}",
"--enablerepo=iridium-iso",
"--cacheonly",
]
else:
logger.warning("ISO repository not found. DNF might try to use network.")
dnf_args = []
cmd = [
"dnf",
"install",
"-y",
f"--installroot={mount_root}",
f"--releasever={releasever}",
"--setopt=install_weak_deps=False",
"--nodocs",
]
if not iso_repo:
cmd.append("--use-host-config")
cmd += dnf_args + packages
with mount_pseudo_fs(mount_root):
run_command(cmd)
logger.info("Base system installation complete.")
log_os_install("INSTALL", "complete", f"Installed to {mount_root}")
def configure_system(mount_root, partition_info, user_info=None, disk_device=None):
"""
Basic configuration: fstab, bootloader, and user creation.
"""
logger.info("Configuring system...")
log_os_install("CONFIGURE", "start", f"Configuring system in {mount_root}")
uefi = is_uefi()
# 1. Generate fstab
def get_uuid(dev):
res = run_command(["blkid", "-s", "UUID", "-o", "value", dev])
return res.stdout.strip()
root_uuid = get_uuid(partition_info["root"])
fstab_lines = [f"UUID={root_uuid} / ext4 defaults 1 1"]
if uefi and partition_info.get("efi"):
efi_uuid = get_uuid(partition_info["efi"])
# For systemd-boot, we mount ESP to /boot
fstab_lines.append(f"UUID={efi_uuid} /boot vfat defaults 0 2")
if partition_info.get("swap"):
swap_uuid = get_uuid(partition_info["swap"])
fstab_lines.append(f"UUID={swap_uuid} none swap defaults 0 0")
os.makedirs(os.path.join(mount_root, "etc"), exist_ok=True)
with open(os.path.join(mount_root, "etc/fstab"), "w") as f:
f.write("\n".join(fstab_lines) + "\n")
# Ensure EFI is mounted for bootloader installation if UEFI
if uefi and partition_info.get("efi"):
efi_target = os.path.join(mount_root, "boot")
os.makedirs(efi_target, exist_ok=True)
# Check if already mounted
res = subprocess.run(["mount"], capture_output=True, text=True)
if efi_target not in res.stdout:
run_command(["mount", partition_info["efi"], efi_target])
with mount_pseudo_fs(mount_root):
# 2. Configure User
if user_info:
logger.info(f"Creating user {user_info['username']}...")
run_command(["chroot", mount_root, "useradd", "-m", "-G", "wheel", user_info["username"]])
# Set hostname
with open(os.path.join(mount_root, "etc/hostname"), "w") as f:
f.write(user_info["hostname"] + "\n")
# Set passwords
try:
res = subprocess.run(
["openssl", "passwd", "-6", user_info["password"]],
capture_output=True,
text=True,
check=True
)
hashed_pass = res.stdout.strip()
run_command(["chroot", mount_root, "usermod", "-p", hashed_pass, user_info["username"]])
run_command(["chroot", mount_root, "usermod", "-p", hashed_pass, "root"])
except Exception as e:
logger.error(f"Failed to set passwords: {e}")
# 3. Configure Bootloader
if uefi:
logger.info("Configuring systemd-boot...")
if not os.path.exists(os.path.join(mount_root, "etc/machine-id")):
run_command(["chroot", mount_root, "systemd-machine-id-setup"])
os.makedirs(os.path.join(mount_root, "etc/kernel"), exist_ok=True)
with open(os.path.join(mount_root, "etc/kernel/cmdline"), "w") as f:
f.write(f"root=UUID={root_uuid} rw quiet\n")
with open(os.path.join(mount_root, "etc/kernel/layout"), "w") as f:
f.write("bls\n")
run_command(["chroot", mount_root, "bootctl", "install", "--path=/boot"])
# Add kernel entries
modules_dir = os.path.join(mount_root, "lib/modules")
if os.path.exists(modules_dir):
kvers = [d for d in os.listdir(modules_dir) if os.path.isdir(os.path.join(modules_dir, d))]
for kver in kvers:
logger.info(f"Adding kernel entry for {kver}...")
initrd_path = f"/boot/initramfs-{kver}.img"
if not os.path.exists(os.path.join(mount_root, initrd_path.lstrip("/"))):
run_command(["chroot", mount_root, "dracut", "--force", initrd_path, kver])
kernel_image = f"/lib/modules/{kver}/vmlinuz"
run_command(["chroot", mount_root, "kernel-install", "add", kver, kernel_image, initrd_path])
else:
logger.info("Configuring GRUB2 (BIOS)...")
# Ensure /etc/default/grub exists
grub_default = os.path.join(mount_root, "etc/default/grub")
if not os.path.exists(grub_default):
with open(grub_default, "w") as f:
f.write('GRUB_TIMEOUT=5\nGRUB_DISTRIBUTOR="$(sed \'s, release .*$,,g\' /etc/system-release)"\nGRUB_DEFAULT=saved\nGRUB_DISABLE_SUBMENU=true\nGRUB_TERMINAL_OUTPUT="console"\nGRUB_CMDLINE_LINUX="rhgb quiet"\nGRUB_DISABLE_RECOVERY="true"\nGRUB_ENABLE_BLSCFG=true\n')
if not disk_device:
disk_device = partition_info["root"].rstrip("0123456789")
if disk_device.endswith("p"): disk_device = disk_device[:-1]
logger.info(f"Installing GRUB to {disk_device} (BIOS)")
run_command(["chroot", mount_root, "grub2-install", "--target=i386-pc", disk_device])
run_command(["chroot", mount_root, "grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"])
run_command(["sync"])
logger.info("System configuration complete.")
log_os_install("CONFIGURE", "complete", "Bootloader and user configured successfully")

View File

@@ -1,31 +1,118 @@
import argparse
import sys import sys
import logging
import gi # Configure logging
logging.basicConfig(level=logging.INFO)
gi.require_version("Gtk", "4.0") # Initialize network logging (TESTING BRANCH ONLY)
gi.require_version("Adw", "1") from .backend.network_logging import init_network_logging, add_discord_handler, send_full_log
from gi.repository import Adw, Gio, Gtk init_network_logging(enabled=True)
add_discord_handler(logging.getLogger("iridium_installer"))
from .ui.window import InstallerWindow logger = logging.getLogger(__name__)
class IridiumInstallerApp(Adw.Application): def main():
def __init__(self): parser = argparse.ArgumentParser(description="Iridium OS Installer")
parser.add_argument(
"--mock",
action="store_true",
help="Run in mock mode (no actual changes to disk)",
)
parser.add_argument(
"--partition-disk",
help="Automatically partition the specified disk (WARNING: DESTROYS DATA)",
)
parser.add_argument(
"--full-install",
help="Run a full minimal installation on the specified disk (WARNING: DESTROYS DATA)",
)
args = parser.parse_args()
if args.partition_disk or args.full_install:
from .backend.disk import auto_partition_disk, mount_partitions
from .backend.os_install import install_minimal_os, configure_system
target = args.partition_disk or args.full_install
logger.info(f"INSTALLER_START: Starting installation on {target}")
try:
print(f"Starting installation on {target}...")
print("Step 1: Partitioning...")
logger.info(f"INSTALLER_PARTITION: Partitioning disk {target}")
parts = auto_partition_disk(target)
logger.info(f"INSTALLER_PARTITION_COMPLETE: Created partitions {parts}")
if args.full_install:
print("Step 2: Mounting...")
logger.info("INSTALLER_MOUNT: Mounting partitions")
mount_root = "/mnt"
mount_partitions(parts, mount_root)
print("Step 3: Installing OS (this may take a while)...")
logger.info("INSTALLER_OS_INSTALL: Installing minimal OS")
install_minimal_os(mount_root)
logger.info("INSTALLER_OS_INSTALL_COMPLETE: OS installed successfully")
print("Step 4: Configuring Bootloader...")
logger.info("INSTALLER_BOOTLOADER: Configuring bootloader")
# Default user for CLI install (since we don't have user input in CLI args yet)
user_info = {
"username": "admin",
"password": "password", # Insecure default for CLI dev testing
"hostname": "iridium-cli"
}
configure_system(mount_root, parts, user_info, disk_device=target)
logger.info("INSTALLER_BOOTLOADER_COMPLETE: Bootloader configured")
print("Installation complete! You can now reboot.")
logger.info(
"INSTALLER_COMPLETE: Full installation finished successfully"
)
else:
print("Partitioning successful!")
print(f"EFI: {parts['efi']}")
print(f"Swap: {parts['swap']}")
print(f"Root: {parts['root']}")
send_full_log()
return 0
except Exception as e:
error_msg = str(e)
print(f"Installation failed: {e}", file=sys.stderr)
logger.error(f"INSTALLER_ERROR: {error_msg}")
import traceback
traceback.print_exc()
logger.error(f"INSTALLER_TRACEBACK: {traceback.format_exc()}")
send_full_log()
return 1
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gio
from .ui.window import InstallerWindow
class IridiumInstallerApp(Adw.Application):
def __init__(self, mock_mode=False):
super().__init__( super().__init__(
application_id="org.iridium.Installer", application_id="org.iridium.Installer",
flags=Gio.ApplicationFlags.FLAGS_NONE, flags=Gio.ApplicationFlags.FLAGS_NONE,
) )
self.mock_mode = mock_mode
def do_activate(self): def do_activate(self):
win = self.props.active_window win = self.props.active_window
if not win: if not win:
win = InstallerWindow(application=self) win = InstallerWindow(application=self, mock_mode=self.mock_mode)
win.present() win.present()
app = IridiumInstallerApp(mock_mode=args.mock)
def main():
app = IridiumInstallerApp()
return app.run(sys.argv) return app.run(sys.argv)

View File

@@ -0,0 +1,101 @@
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk
class ModulesPage(Adw.Bin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.nvidia_drivers = False
self.chromebook_audio = False
self.android_apps = False
self.module_titles = {
"nvidia_drivers": "Proprietary NVIDIA Drivers",
"chromebook_audio": "Chromebook Audio Fixes",
"android_apps": "Android Apps Support",
}
# Main Layout
clamp = Adw.Clamp()
clamp.set_maximum_size(600)
self.set_child(clamp)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(24)
box.set_valign(Gtk.Align.CENTER)
box.set_margin_top(24)
box.set_margin_bottom(24)
clamp.set_child(box)
# Title
title = Gtk.Label(label="Additional Modules")
title.add_css_class("title-1")
box.append(title)
descr = Gtk.Label(
label="Select additional components you would like to install. (Internet connection required)"
)
descr.set_wrap(True)
box.append(descr)
dont_worry = Gtk.Label(label="Don't worry, you can always install them later.")
dont_worry.set_wrap(True)
box.append(dont_worry)
# Preferences Group
self.modules_group = Adw.PreferencesGroup()
self.modules_group.set_title("Optional Features")
box.append(self.modules_group)
# NVIDIA Drivers
self.add_module_row(
self.module_titles["nvidia_drivers"],
"Install proprietary drivers for NVIDIA graphics cards for better performance.",
"nvidia_drivers",
)
# Chromebook Audio
self.add_module_row(
self.module_titles["chromebook_audio"],
"Install additional audio drivers for Chromebook devices.",
"chromebook_audio",
)
# Android Apps
self.add_module_row(
self.module_titles["android_apps"],
"Install Waydroid to run Android applications on Iridium OS.",
"android_apps",
)
def add_module_row(self, title, subtitle, attr_name):
row = Adw.ActionRow()
row.set_title(title)
row.set_subtitle(subtitle)
switch = Gtk.Switch()
switch.set_valign(Gtk.Align.CENTER)
# Set initial state
switch.set_active(getattr(self, attr_name))
# Connect signal
switch.connect("notify::active", self.on_switch_toggled, attr_name)
row.add_suffix(switch)
self.modules_group.add(row)
def on_switch_toggled(self, switch, gparam, attr_name):
setattr(self, attr_name, switch.get_active())
def get_modules(self):
enabled_modules = []
if self.nvidia_drivers:
enabled_modules.append(self.module_titles["nvidia_drivers"])
if self.chromebook_audio:
enabled_modules.append(self.module_titles["chromebook_audio"])
if self.android_apps:
enabled_modules.append(self.module_titles["android_apps"])
return enabled_modules

View File

@@ -0,0 +1,74 @@
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk
class InstallModePage(Adw.Bin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.mode = "automatic"
# Main Layout
clamp = Adw.Clamp()
clamp.set_maximum_size(600)
self.set_child(clamp)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(24)
box.set_valign(Gtk.Align.CENTER) # Vertically Center
box.set_margin_top(24)
box.set_margin_bottom(24)
clamp.set_child(box)
# Title
title = Gtk.Label(label="Installation Type")
title.add_css_class("title-1")
box.append(title)
descr = Gtk.Label(label="How would you like to install Iridium OS?")
descr.set_wrap(True)
box.append(descr)
# Selection Group
group = Adw.PreferencesGroup()
box.append(group)
# Automatic
self.auto_row = Adw.ActionRow()
self.auto_row.set_title("Automatic")
self.auto_row.set_subtitle(
"Erase the selected disk and install Iridium. (Recommended)"
)
self.auto_row.set_icon_name("drive-harddisk-solidstate-symbolic")
group.add(self.auto_row)
self.auto_radio = Gtk.CheckButton()
self.auto_radio.set_active(True)
self.auto_radio.connect("toggled", self.on_mode_toggled, "automatic")
self.auto_row.add_suffix(self.auto_radio)
self.auto_row.set_activatable_widget(self.auto_radio)
# Manual
self.manual_row = Adw.ActionRow()
self.manual_row.set_title("Manual Partitioning")
self.manual_row.set_subtitle(
"Create or resize partitions yourself. For advanced users."
)
self.manual_row.set_icon_name("preferences-system-symbolic")
group.add(self.manual_row)
self.manual_radio = Gtk.CheckButton()
self.manual_radio.set_group(self.auto_radio)
self.manual_radio.connect("toggled", self.on_mode_toggled, "manual")
self.manual_row.add_suffix(self.manual_radio)
self.manual_row.set_activatable_widget(self.manual_radio)
def on_mode_toggled(self, button, mode_name):
if button.get_active():
self.mode = mode_name
def get_mode(self):
return self.mode

View File

@@ -1,4 +1,5 @@
import json import json
import os
import subprocess import subprocess
import gi import gi
@@ -41,6 +42,114 @@ CSS = """
""" """
def get_total_memory() -> int:
try:
return os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
except (ValueError, OSError):
with open("/proc/meminfo", "r") as f:
for line in f:
if line.startswith("MemTotal:"):
parts = line.split()
return int(parts[1]) * 1024
return 0
def is_uefi():
return os.path.exists("/sys/firmware/efi")
def calculate_auto_partitions(disk_device):
disk_size = 0
uefi = is_uefi()
try:
# Get disk size in bytes
result = subprocess.run(
["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE"],
capture_output=True,
text=True,
)
if result.returncode == 0:
data = json.loads(result.stdout)
devices = data.get("blockdevices", [])
dev_name = disk_device.replace("/dev/", "")
for dev in devices:
if dev.get("name") == dev_name:
disk_size = int(dev.get("size", 0))
break
except Exception as e:
print(f"Error getting disk size for auto partitioning: {e}")
return []
if disk_size == 0:
return []
ram_size = get_total_memory()
disk_mb = disk_size / (1024 * 1024)
# Defaults
efi_size = 1024 * 1024 * 1024 if uefi else 0
swap_size = ram_size + (2 * 1024 * 1024 * 1024)
min_root_size = 10 * 1024 * 1024 * 1024 # 10GB
# Cap swap for auto-calc if disk is small
if disk_size < 100 * 1024**3:
swap_size = min(swap_size, 4 * 1024**3)
total_required = efi_size + swap_size + min_root_size
use_swap = True
if disk_size < total_required:
if uefi:
efi_size = 512 * 1024 * 1024
use_swap = False
swap_size = 0
if disk_size < (efi_size + min_root_size):
print("Disk too small for automatic partitioning scheme.")
return []
root_size = disk_size - efi_size - swap_size
partitions = []
if uefi:
partitions.append({
"type": "partition",
"name": "EFI System",
"filesystem": "vfat",
"mount_point": "/boot/efi",
"size": f"{efi_size / (1024**3):.1f} GB",
"bytes": efi_size,
"style_class": "part-efi",
})
partitions.append({
"type": "partition",
"name": "Root",
"filesystem": "ext4",
"mount_point": "/",
"size": f"{root_size / (1024**3):.1f} GB",
"bytes": root_size,
"style_class": "part-root",
})
if use_swap:
partitions.append(
{
"type": "partition",
"name": "Swap",
"filesystem": "swap",
"mount_point": "[SWAP]",
"size": f"{swap_size / (1024**3):.1f} GB",
"bytes": swap_size,
"style_class": "part-swap",
}
)
return partitions
class PartitionSegment(Gtk.Button): class PartitionSegment(Gtk.Button):
def __init__(self, part_data, page, *args, **kwargs): def __init__(self, part_data, page, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -90,11 +199,12 @@ class PartitioningPage(Adw.Bin):
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(24) box.set_spacing(24)
box.set_valign(Gtk.Align.CENTER)
box.set_margin_top(24) box.set_margin_top(24)
box.set_margin_bottom(24) box.set_margin_bottom(24)
clamp.set_child(box) clamp.set_child(box)
title = Gtk.Label(label="Disk Configuration") title = Gtk.Label(label="Manual Partitioning")
title.add_css_class("title-1") title.add_css_class("title-1")
box.append(title) box.append(title)
@@ -143,6 +253,7 @@ class PartitioningPage(Adw.Bin):
# Initially empty until loaded with a specific disk # Initially empty until loaded with a specific disk
def load_partitions(self, disk_device=None): def load_partitions(self, disk_device=None):
self.current_disk_path = disk_device
target_disk = None target_disk = None
try: try:
@@ -197,7 +308,7 @@ class PartitioningPage(Adw.Bin):
p_size = int(p.get("size", 0)) p_size = int(p.get("size", 0))
covered_size += p_size covered_size += p_size
fstype = p.get("fstype", "").lower() fstype = (p.get("fstype") or "").lower()
style = "part-root" style = "part-root"
if "fat" in fstype or "efi" in fstype: if "fat" in fstype or "efi" in fstype:
style = "part-efi" style = "part-efi"
@@ -208,7 +319,7 @@ class PartitioningPage(Adw.Bin):
{ {
"type": "partition", "type": "partition",
"name": p.get("name"), "name": p.get("name"),
"filesystem": p.get("fstype", "Unknown"), "filesystem": p.get("fstype") or "Unknown",
"label": p.get("name"), "label": p.get("name"),
"mount_point": p.get("mountpoint", ""), "mount_point": p.get("mountpoint", ""),
"size": self.format_size(p_size), "size": self.format_size(p_size),
@@ -335,43 +446,184 @@ class PartitioningPage(Adw.Bin):
def show_context_menu(self, widget, x, y): def show_context_menu(self, widget, x, y):
data = widget.part_data data = widget.part_data
self.selected_disk_path = self.current_disk_path # Assumes we store this
popover = Gtk.Popover() popover = Gtk.Popover()
popover.set_parent(widget) popover.set_parent(widget)
menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
menu_box.set_spacing(0)
popover.set_child(menu_box) popover.set_child(menu_box)
def add_menu_item(label, icon_name=None, destructive=False): def add_menu_item(label, icon_name, callback, destructive=False):
btn = Gtk.Button(label=label) btn = Gtk.Button(label=label)
btn.add_css_class("flat") btn.add_css_class("flat")
btn.set_halign(Gtk.Align.FILL)
if destructive: if destructive:
btn.add_css_class("destructive-action") btn.add_css_class("destructive-action")
if icon_name: content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
content.set_spacing(12)
icon = Gtk.Image.new_from_icon_name(icon_name) icon = Gtk.Image.new_from_icon_name(icon_name)
lbl = Gtk.Label(label=label) lbl = Gtk.Label(label=label)
content.append(icon) content.append(icon)
content.append(lbl) content.append(lbl)
btn.set_child(content) btn.set_child(content)
btn.connect("clicked", lambda b: [popover.popdown(), callback()])
menu_box.append(btn) menu_box.append(btn)
return btn
if data.get("type") == "partition": if data.get("type") == "partition":
add_menu_item("Select Mount Point", "folder-open-symbolic") add_menu_item(
add_menu_item("Format", "drive-harddisk-symbolic") "Select Mount Point",
add_menu_item("Resize", "object-resize-symbolic") "folder-open-symbolic",
lambda: self.select_mount_point(data),
)
separator = Gtk.Separator() separator = Gtk.Separator()
menu_box.append(separator) menu_box.append(separator)
btn_del = add_menu_item("Delete", "user-trash-symbolic", destructive=True) add_menu_item(
btn_del.add_css_class("error") "Delete",
"user-trash-symbolic",
lambda: self.delete_part(data),
destructive=True,
)
elif data.get("type") == "empty": elif data.get("type") == "empty":
add_menu_item("Create Partition", "list-add-symbolic") add_menu_item(
"Create Partition",
"list-add-symbolic",
lambda: self.create_part_dialog(data),
)
popover.popup() popover.popup()
def select_mount_point(self, data):
win = Adw.Window(
title="Select Mount Point", modal=True, transient_for=self.get_root()
)
box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=12,
margin_top=24,
margin_bottom=24,
margin_start=24,
margin_end=24,
)
win.set_content(box)
options = ["/", "/boot", "[SWAP]", "None"]
dropdown = Gtk.DropDown.new_from_strings(options)
current_mp = data.get("mount_point")
# Ensure we have a valid string comparison
if not current_mp:
current_mp = "None"
if current_mp in options:
dropdown.set_selected(options.index(current_mp))
else:
dropdown.set_selected(options.index("None"))
box.append(Gtk.Label(label=f"Mount point for {data['name']}:"))
box.append(dropdown)
def on_apply(b):
selected = options[dropdown.get_selected()]
data["mount_point"] = selected if selected != "None" else ""
win.close()
self.refresh_bar()
btn = Gtk.Button(label="Apply")
btn.add_css_class("suggested-action")
btn.connect("clicked", on_apply)
box.append(btn)
win.present()
def delete_part(self, data):
# Extract partition number from name (e.g., nvme0n1p3 -> 3)
import re
from ...backend.disk import delete_partition
match = re.search(r"(\d+)$", data["name"])
if match:
part_num = int(match.group(1))
delete_partition(self.current_disk_path, part_num)
self.load_partitions(self.current_disk_path)
def create_part_dialog(self, data):
win = Adw.Window(
title="Create Partition", modal=True, transient_for=self.get_root()
)
box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=12,
margin_top=24,
margin_bottom=24,
margin_start=24,
margin_end=24,
)
win.set_content(box)
# Type dropdown
types = {"Root (Linux)": "8300", "EFI System": "ef00", "Swap": "8200"}
type_names = list(types.keys())
type_dropdown = Gtk.DropDown.new_from_strings(type_names)
box.append(Gtk.Label(label="Partition Type:"))
box.append(type_dropdown)
# Size entry
size_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
size_entry = Gtk.Entry(text=str(int(data["bytes"] / (1024 * 1024))))
size_entry.set_width_chars(15)
size_box.append(size_entry)
size_box.append(Gtk.Label(label="MB"))
box.append(Gtk.Label(label="Size:"))
box.append(size_box)
error_label = Gtk.Label(label="")
error_label.add_css_class("error")
box.append(error_label)
def on_create(b):
from ...backend.disk import create_partition
selected_type = type_names[type_dropdown.get_selected()]
# Default name and fstype based on type
if selected_type == "EFI System":
name = "EFI System"
fstype = "fat32"
elif selected_type == "Swap":
name = "Swap"
fstype = "swap"
else:
name = "Linux filesystem"
fstype = "ext4"
size_text = size_entry.get_text()
try:
size_mb = int(size_text)
max_mb = int(data["bytes"] / (1024 * 1024))
if size_mb >= max_mb:
size_mb = 0
except ValueError:
error_label.set_text("Invalid size value")
return
type_code = types[selected_type]
try:
create_partition(
self.current_disk_path, size_mb, type_code, name, fstype
)
win.close()
self.load_partitions(self.current_disk_path)
except Exception as e:
error_label.set_text(f"Error: {str(e)}")
btn = Gtk.Button(label="Create")
btn.add_css_class("suggested-action")
btn.connect("clicked", on_create)
box.append(btn)
win.present()
def get_config(self):
return {"partitions": self.partitions}

View File

@@ -6,10 +6,14 @@ import gi
gi.require_version("Gtk", "4.0") gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1") gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk from gi.repository import Adw, GObject, Gtk
class StoragePage(Adw.Bin): class StoragePage(Adw.Bin):
__gsignals__ = {
"disk-selected": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -22,6 +26,7 @@ class StoragePage(Adw.Bin):
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(24) box.set_spacing(24)
box.set_valign(Gtk.Align.CENTER)
box.set_margin_top(24) box.set_margin_top(24)
box.set_margin_bottom(24) box.set_margin_bottom(24)
clamp.set_child(box) clamp.set_child(box)
@@ -67,8 +72,7 @@ class StoragePage(Adw.Bin):
if not self.first_radio: if not self.first_radio:
radio = Gtk.CheckButton() radio = Gtk.CheckButton()
self.first_radio = radio self.first_radio = radio
# Default selection # No default selection to force user interaction, for safety reasons
self.selected_disk = dev
else: else:
radio = Gtk.CheckButton() radio = Gtk.CheckButton()
radio.set_group(self.first_radio) radio.set_group(self.first_radio)
@@ -85,7 +89,13 @@ class StoragePage(Adw.Bin):
def on_disk_toggled(self, button, device_name): def on_disk_toggled(self, button, device_name):
if button.get_active(): if button.get_active():
self.selected_disk = device_name # Prepend /dev/ if it's just the name
full_path = device_name
if not device_name.startswith("/dev/"):
full_path = f"/dev/{device_name}"
self.selected_disk = full_path
self.emit("disk-selected", full_path)
def get_selected_disk(self): def get_selected_disk(self):
return self.selected_disk return self.selected_disk

View File

@@ -0,0 +1,85 @@
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk
class SummaryPage(Adw.Bin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.status_page = Adw.StatusPage()
self.status_page.set_title("Summary")
self.status_page.set_description("Review your choices before installing")
self.status_page.set_icon_name("document-save-symbolic")
self.set_child(self.status_page)
# Main content box
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(24)
clamp = Adw.Clamp()
clamp.set_maximum_size(600)
self.status_page.set_child(clamp)
content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
content_box.set_spacing(24)
clamp.set_child(content_box)
# Storage and partitioning
self.storage_group = Adw.PreferencesGroup()
self.storage_group.set_title("Storage & Partitioning")
content_box.append(self.storage_group)
self.disk_row = Adw.ActionRow()
self.disk_row.set_title("Target Disk")
self.storage_group.add(self.disk_row)
self.mode_row = Adw.ActionRow()
self.mode_row.set_title("Partitioning Mode")
self.storage_group.add(self.mode_row)
# User configuration
self.user_group = Adw.PreferencesGroup()
self.user_group.set_title("User Configuration")
content_box.append(self.user_group)
self.fullname_row = Adw.ActionRow()
self.fullname_row.set_title("Full Name")
self.user_group.add(self.fullname_row)
self.username_row = Adw.ActionRow()
self.username_row.set_title("Username")
self.user_group.add(self.username_row)
self.hostname_row = Adw.ActionRow()
self.hostname_row.set_title("Hostname")
self.user_group.add(self.hostname_row)
# Software
self.software_group = Adw.PreferencesGroup()
self.software_group.set_title("Software")
content_box.append(self.software_group)
self.modules_row = Adw.ActionRow()
self.modules_row.set_title("Additional Modules")
self.software_group.add(self.modules_row)
def update_summary(self, disk_info, mode, partitions, user_info, modules):
# Update Disk
self.disk_row.set_subtitle(str(disk_info))
# Update Mode
self.mode_row.set_subtitle(mode.capitalize())
# Update User Info
self.fullname_row.set_subtitle(user_info.get("fullname", ""))
self.username_row.set_subtitle(user_info.get("username", ""))
self.hostname_row.set_subtitle(user_info.get("hostname", ""))
# Update Modules
if modules:
self.modules_row.set_subtitle(", ".join(modules))
else:
self.modules_row.set_subtitle("None")

View File

@@ -1,20 +1,29 @@
import re
import gi import gi
gi.require_version("Gtk", "4.0") gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1") gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk from gi.repository import Adw, GObject, Gtk
class UserPage(Adw.Bin): class UserPage(Adw.Bin):
__gsignals__ = {
"validity-changed": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._is_valid = False
clamp = Adw.Clamp() clamp = Adw.Clamp()
clamp.set_maximum_size(500) clamp.set_maximum_size(500)
self.set_child(clamp) self.set_child(clamp)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_spacing(24) box.set_spacing(24)
box.set_valign(Gtk.Align.CENTER)
box.set_margin_top(24) box.set_margin_top(24)
box.set_margin_bottom(24) box.set_margin_bottom(24)
clamp.set_child(box) clamp.set_child(box)
@@ -29,18 +38,28 @@ class UserPage(Adw.Bin):
self.fullname_row = Adw.EntryRow() self.fullname_row = Adw.EntryRow()
self.fullname_row.set_title("Full Name") self.fullname_row.set_title("Full Name")
self.fullname_row.set_text("Administrator")
self.fullname_row.connect("notify::text", self.on_fullname_changed)
self.fullname_row.connect("notify::text", self.on_input_changed)
group.add(self.fullname_row) group.add(self.fullname_row)
self.username_row = Adw.EntryRow() self.username_row = Adw.EntryRow()
self.username_row.set_title("Username") self.username_row.set_title("Username")
self.username_row.set_text("admin")
self.username_handler_id = self.username_row.connect(
"notify::text", self.on_username_changed
)
self.username_row.connect("notify::text", self.on_input_changed)
group.add(self.username_row) group.add(self.username_row)
self.password_row = Adw.PasswordEntryRow() self.password_row = Adw.PasswordEntryRow()
self.password_row.set_title("Password") self.password_row.set_title("Password")
self.password_row.connect("notify::text", self.on_input_changed)
group.add(self.password_row) group.add(self.password_row)
self.confirm_row = Adw.PasswordEntryRow() self.confirm_row = Adw.PasswordEntryRow()
self.confirm_row.set_title("Confirm Password") self.confirm_row.set_title("Confirm Password")
self.confirm_row.connect("notify::text", self.on_input_changed)
group.add(self.confirm_row) group.add(self.confirm_row)
# Hostname # Hostname
@@ -50,16 +69,117 @@ class UserPage(Adw.Bin):
self.hostname_row = Adw.EntryRow() self.hostname_row = Adw.EntryRow()
self.hostname_row.set_title("Hostname") self.hostname_row.set_title("Hostname")
self.hostname_row.set_text("iridium-pc") self.hostname_row.set_text("iridium")
self.hostname_row.connect("notify::text", self.on_input_changed)
host_group.add(self.hostname_row) host_group.add(self.hostname_row)
# Administrator self.username_manually_set = False
admin_group = Adw.PreferencesGroup()
admin_group.set_margin_top(12)
box.append(admin_group)
admin_row = Adw.SwitchRow() # Initial validation
admin_row.set_title("Make this user administrator") self.validate()
admin_row.set_subtitle("Add to sudoers group")
admin_row.set_active(True) def on_fullname_changed(self, entry, _pspec):
admin_group.add(admin_row) if self.username_manually_set:
return
username = entry.get_text().lower()
if username == "administrator":
username = "admin"
for sign in (
" ",
"-",
"_",
".",
",",
":",
";",
"!",
"?",
"/",
"\\",
"|",
"@",
"#",
"$",
"%",
"^",
"&",
"*",
"(",
")",
"[",
"]",
"{",
"}",
"'",
'"',
"`",
"~",
"<",
">",
"=",
"+",
):
username = username.replace(sign, "_")
self.username_row.handler_block(self.username_handler_id)
self.username_row.set_text(username)
self.username_row.handler_unblock(self.username_handler_id)
def on_username_changed(self, entry, _pspec):
self.username_manually_set = True
def on_input_changed(self, *args):
self.validate()
def validate(self):
valid = True
# Username validation
username = self.username_row.get_text()
if not username:
valid = False
self.username_row.add_css_class("error")
elif not re.match(r"^[a-z_][a-z0-9_-]*$", username):
valid = False
self.username_row.add_css_class("error")
else:
self.username_row.remove_css_class("error")
# Password validation
pw = self.password_row.get_text()
confirm = self.confirm_row.get_text()
if not pw:
valid = False
# Don't show error for empty password initially, just invalidate
# It looks a lot nicer like that
if pw != confirm:
valid = False
self.confirm_row.add_css_class("error")
else:
self.confirm_row.remove_css_class("error")
# Hostname validation
hostname = self.hostname_row.get_text()
if not hostname:
valid = False
self.hostname_row.add_css_class("error")
else:
self.hostname_row.remove_css_class("error")
if self._is_valid != valid:
self._is_valid = valid
self.emit("validity-changed", valid)
def is_valid(self):
return self._is_valid
def get_user_info(self):
return {
"fullname": self.fullname_row.get_text(),
"username": self.username_row.get_text(),
"password": self.password_row.get_text(),
"hostname": self.hostname_row.get_text(),
}

View File

@@ -1,8 +1,10 @@
import os
import gi import gi
gi.require_version("Gtk", "4.0") gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1") gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk from gi.repository import Adw, Gdk, Gtk
class WelcomePage(Adw.Bin): class WelcomePage(Adw.Bin):
@@ -11,7 +13,25 @@ class WelcomePage(Adw.Bin):
page = Adw.StatusPage() page = Adw.StatusPage()
page.set_title("Welcome to Iridium OS") page.set_title("Welcome to Iridium OS")
page.set_description("The future of computing is here. Let's get you set up.") page.set_description("The installation will begin shortly")
# Load custom icon
icon_path = os.path.join(
os.path.dirname(__file__),
"..",
"..",
"..",
"data",
"icons",
"org.iridium.Installer.svg",
)
if os.path.exists(icon_path):
try:
paintable = Gdk.Texture.new_from_filename(icon_path)
page.set_paintable(paintable)
except Exception:
page.set_icon_name("system-software-install-symbolic")
else:
page.set_icon_name("system-software-install-symbolic") page.set_icon_name("system-software-install-symbolic")
# Content Box # Content Box
@@ -25,15 +45,9 @@ class WelcomePage(Adw.Bin):
# Language Dropdown # Language Dropdown
languages = [ languages = [
"English (US)", "English",
"Spanish",
"French",
"German",
"Japanese",
"Chinese (Simplified)",
] ]
dropdown = Gtk.DropDown.new_from_strings(languages) dropdown = Gtk.DropDown.new_from_strings(languages)
dropdown.set_margin_bottom(20)
box.append(dropdown) box.append(dropdown)
page.set_child(box) page.set_child(box)

View File

@@ -1,61 +1,90 @@
import gi import gi
import threading
import logging
import uuid
from datetime import datetime
gi.require_version("Gtk", "4.0") gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1") gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk from gi.repository import Adw, Gtk, GLib
from .pages.partitioning import PartitioningPage from .pages.additional_modules import ModulesPage
from .pages.install_mode import InstallModePage
from .pages.partitioning import PartitioningPage, calculate_auto_partitions
from .pages.storage import StoragePage from .pages.storage import StoragePage
from .pages.summary import SummaryPage
from .pages.user import UserPage from .pages.user import UserPage
from .pages.welcome import WelcomePage from .pages.welcome import WelcomePage
from ..backend.network_logging import log_to_discord, flush_logs, send_full_log
class LogHandler(logging.Handler):
def __init__(self, callback):
super().__init__()
self.callback = callback
def emit(self, record):
msg = self.format(record)
GLib.idle_add(self.callback, msg)
class InstallerWindow(Adw.ApplicationWindow): class InstallerWindow(Adw.ApplicationWindow):
def __init__(self, *args, **kwargs): def __init__(self, mock_mode=False, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.mock_mode = mock_mode
self.session_id = str(uuid.uuid4())[:8]
# Send session start ping
mode_str = "MOCK" if mock_mode else "PRODUCTION"
log_to_discord(
"SESSION_START",
f"Iridium Installer started - Session: {self.session_id} - Mode: {mode_str}",
"installer",
)
self.set_default_size(900, 650) self.set_default_size(900, 650)
self.set_title("Iridium Installer") self.set_title("Iridium Installer" + (" (MOCK MODE)" if mock_mode else ""))
self.set_icon_name("org.iridium.Installer")
# Main Layout self.toolbar_view = Adw.ToolbarView()
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.set_content(self.toolbar_view)
self.set_content(main_box)
# Header Bar # Header Bar (Top)
header = Adw.HeaderBar() self.header_bar = Adw.HeaderBar()
main_box.append(header) self.toolbar_view.add_top_bar(self.header_bar)
# Content Stack # Content Stack
self.stack = Adw.ViewStack() self.stack = Adw.ViewStack()
self.stack.set_vexpand(True) self.stack.set_vexpand(True)
main_box.append(self.stack) self.toolbar_view.set_content(self.stack)
# Navigation Bar (Bottom) # Navigation Bar (Bottom)
nav_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.bottom_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
nav_box.add_css_class("toolbar") self.bottom_bar.add_css_class("toolbar")
nav_box.set_margin_top(12) self.bottom_bar.set_margin_top(12)
nav_box.set_margin_bottom(12) self.bottom_bar.set_margin_bottom(12)
nav_box.set_margin_start(12) self.bottom_bar.set_margin_start(12)
nav_box.set_margin_end(12) self.bottom_bar.set_margin_end(12)
nav_box.set_spacing(12) self.bottom_bar.set_spacing(12)
main_box.append(nav_box) self.toolbar_view.add_bottom_bar(self.bottom_bar)
# Back Button # Back Button
self.back_button = Gtk.Button(label="Back") self.back_button = Gtk.Button(label="Back")
self.back_button.connect("clicked", self.on_back_clicked) self.back_button.connect("clicked", self.on_back_clicked)
self.back_button.set_sensitive(False) self.back_button.set_sensitive(False)
nav_box.append(self.back_button) self.bottom_bar.append(self.back_button)
# Spacer to push Next button to the right # Spacer to push Next button to the right
spacer = Gtk.Label() spacer = Gtk.Label()
spacer.set_hexpand(True) spacer.set_hexpand(True)
nav_box.append(spacer) self.bottom_bar.append(spacer)
# Next Button # Next Button
self.next_button = Gtk.Button(label="Next") self.next_button = Gtk.Button(label="Next")
self.next_button.add_css_class("suggested-action") self.next_button.add_css_class("suggested-action")
self.next_button.connect("clicked", self.on_next_clicked) self.next_button.connect("clicked", self.on_next_clicked)
nav_box.append(self.next_button) self.bottom_bar.append(self.next_button)
# Page Management # Page Management
self.page_ids = [] self.page_ids = []
@@ -64,30 +93,60 @@ class InstallerWindow(Adw.ApplicationWindow):
# Initialize Pages # Initialize Pages
self.welcome_page = WelcomePage() self.welcome_page = WelcomePage()
self.storage_page = StoragePage() self.storage_page = StoragePage()
self.storage_page.connect("disk-selected", self.on_disk_selected)
self.install_mode_page = InstallModePage()
self.partitioning_page = PartitioningPage() self.partitioning_page = PartitioningPage()
self.modules_page = ModulesPage()
self.user_page = UserPage() self.user_page = UserPage()
self.user_page.connect("validity-changed", self.on_user_validity_changed)
self.summary_page = SummaryPage()
# Add Pages # Add Pages
self.add_page(self.welcome_page, "welcome") self.add_page(self.welcome_page, "welcome")
self.add_page(self.storage_page, "storage") self.add_page(self.storage_page, "storage")
self.add_page(self.install_mode_page, "install_mode")
self.add_page(self.partitioning_page, "partitioning") self.add_page(self.partitioning_page, "partitioning")
self.add_page(self.modules_page, "modules")
self.add_page(self.user_page, "user") self.add_page(self.user_page, "user")
self.add_page(self.summary_page, "summary")
# Initialize view # Initialize view
if self.page_ids: if self.page_ids:
self.stack.set_visible_child_name(self.page_ids[0]) self.stack.set_visible_child_name(self.page_ids[0])
self.update_buttons()
self.log_handler = None
def add_page(self, widget, name): def add_page(self, widget, name):
self.stack.add_named(widget, name) self.stack.add_named(widget, name)
self.page_ids.append(name) self.page_ids.append(name)
def on_disk_selected(self, page, device_name):
self.update_buttons()
def on_user_validity_changed(self, page, valid):
self.update_buttons()
def update_buttons(self): def update_buttons(self):
# Back button state # Back button state
self.back_button.set_sensitive(self.current_page_index > 0) self.back_button.set_sensitive(self.current_page_index > 0)
# Next button label/state # Next button label/state
if self.current_page_index == len(self.page_ids) - 1: current_page_name = self.page_ids[self.current_page_index]
self.next_button.set_label("Install")
# Forced selection logic
forced_selection = True
if current_page_name == "storage":
forced_selection = self.storage_page.get_selected_disk() is not None
elif current_page_name == "user":
forced_selection = self.user_page.is_valid()
self.next_button.set_sensitive(forced_selection)
if current_page_name == "summary":
self.next_button.set_label(
"Install" + (" (MOCK)" if self.mock_mode else "")
)
self.next_button.add_css_class("destructive-action") self.next_button.add_css_class("destructive-action")
self.next_button.remove_css_class("suggested-action") self.next_button.remove_css_class("suggested-action")
else: else:
@@ -96,27 +155,341 @@ class InstallerWindow(Adw.ApplicationWindow):
self.next_button.add_css_class("suggested-action") self.next_button.add_css_class("suggested-action")
def on_back_clicked(self, button): def on_back_clicked(self, button):
if self.current_page_index > 0: current_page = self.page_ids[self.current_page_index]
self.current_page_index -= 1
# Default: go back one page
next_prev_index = self.current_page_index - 1
if current_page == "modules":
mode = self.install_mode_page.get_mode()
if mode == "automatic":
# Skip partitioning (which is immediately before modules)
next_prev_index = self.page_ids.index("install_mode")
if next_prev_index >= 0:
self.current_page_index = next_prev_index
self.stack.set_visible_child_name(self.page_ids[self.current_page_index]) self.stack.set_visible_child_name(self.page_ids[self.current_page_index])
self.update_buttons() self.update_buttons()
def show_progress_page(self, message):
prog_page = Adw.StatusPage()
prog_page.set_title(message)
prog_page.set_description("Please wait while we set up your system.")
spinner = Gtk.Spinner()
spinner.set_size_request(32, 32)
spinner.set_halign(Gtk.Align.CENTER)
spinner.start()
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
box.append(spinner)
# Log view
self.log_buffer = Gtk.TextBuffer()
self.log_view = Gtk.TextView(buffer=self.log_buffer)
self.log_view.set_editable(False)
self.log_view.set_monospace(True)
self.log_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
scrolled = Gtk.ScrolledWindow()
scrolled.set_child(self.log_view)
scrolled.set_vexpand(True)
scrolled.set_size_request(-1, 200)
# Style the log view background
# css_provider = Gtk.CssProvider()
# css_provider.load_from_data(b"textview { background-color: #1e1e1e; color: #ffffff; }")
# self.log_view.get_style_context().add_provider(css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
expander = Gtk.Expander(label="Detailed Logs")
expander.set_child(scrolled)
box.append(expander)
prog_page.set_child(box)
name = "progress_install"
self.stack.add_named(prog_page, name)
self.stack.set_visible_child_name(name)
# Hide navigation buttons during install
self.bottom_bar.set_visible(False)
# Attach log handler
self.log_handler = LogHandler(self.append_log)
logging.getLogger().addHandler(self.log_handler)
logging.getLogger().setLevel(logging.INFO)
def append_log(self, msg):
end_iter = self.log_buffer.get_end_iter()
self.log_buffer.insert(end_iter, msg + "\n")
# Auto-scroll
mark = self.log_buffer.create_mark("end", end_iter, False)
self.log_view.scroll_to_mark(mark, 0.0, True, 0.0, 1.0)
def show_finish_page(self, message, success=True):
if self.log_handler:
logging.getLogger().removeHandler(self.log_handler)
self.log_handler = None
# Release wake lock if held
if hasattr(self, "inhibit_cookie") and self.inhibit_cookie:
app = self.get_application()
app.uninhibit(self.inhibit_cookie)
self.inhibit_cookie = None
finish_page = Adw.StatusPage()
finish_page.set_title(message)
btn = Gtk.Button()
btn.set_halign(Gtk.Align.CENTER)
if success:
finish_page.set_icon_name("emblem-ok-symbolic")
finish_page.set_description("You can now restart your computer.")
btn.set_label("Restart System")
btn.add_css_class("suggested-action")
btn.add_css_class("pill")
def on_restart(b):
import subprocess
try:
# Use sudo since we might be running as liveuser
subprocess.run(["sudo", "systemctl", "reboot"])
except Exception as e:
print(f"Failed to reboot: {e}")
self.close()
btn.connect("clicked", on_restart)
else:
finish_page.set_icon_name("dialog-error-symbolic")
finish_page.set_description("An error occurred during installation.")
btn.set_label("Close Installer")
btn.connect("clicked", lambda b: self.close())
finish_page.set_child(btn)
name = "finish_install"
self.stack.add_named(finish_page, name)
self.stack.set_visible_child_name(name)
def run_installation(self, disk, mode, modules, user_info):
try:
from ..backend.disk import auto_partition_disk, mount_partitions
from ..backend.os_install import install_minimal_os, configure_system
from ..backend.network_logging import log_to_discord as nlog
# Step 1: Partitioning
logging.info("Step 1: Partitioning...")
nlog(
"INSTALL_PROGRESS",
f"Session: {self.session_id} - Starting partitioning on {disk}",
"installer",
)
parts = auto_partition_disk(disk)
nlog(
"INSTALL_PROGRESS",
f"Session: {self.session_id} - Partitioning complete",
"installer",
)
# Step 2: Mounting
logging.info("Step 2: Mounting...")
nlog(
"INSTALL_PROGRESS",
f"Session: {self.session_id} - Mounting partitions",
"installer",
)
mount_root = "/mnt"
mount_partitions(parts, mount_root)
nlog(
"INSTALL_PROGRESS",
f"Session: {self.session_id} - Mounting complete",
"installer",
)
# Step 3: OS Installation
logging.info("Step 3: OS Installation...")
nlog(
"INSTALL_PROGRESS",
f"Session: {self.session_id} - Installing OS (this may take a while)",
"installer",
)
install_minimal_os(mount_root)
nlog(
"INSTALL_PROGRESS",
f"Session: {self.session_id} - OS installation complete",
"installer",
)
# Step 4: Configure
logging.info("Step 4: Configuration...")
nlog(
"INSTALL_PROGRESS",
f"Session: {self.session_id} - Configuring bootloader and user",
"installer",
)
configure_system(mount_root, parts, user_info, disk_device=disk)
nlog(
"INSTALL_PROGRESS",
f"Session: {self.session_id} - Configuration complete",
"installer",
)
# Install complete
nlog(
"INSTALL_COMPLETE",
f"Session: {self.session_id} - Installation completed successfully!",
"installer",
)
flush_logs()
send_full_log()
GLib.idle_add(self.show_finish_page, "Installation Successful!", True)
except Exception as e:
error_msg = str(e)
logging.error(f"Installation failed: {e}")
nlog(
"INSTALL_ERROR",
f"Session: {self.session_id} - Installation failed: {error_msg}",
"installer",
)
import traceback
traceback.print_exc()
nlog(
"INSTALL_ERROR",
f"Session: {self.session_id} - Traceback: {traceback.format_exc()}",
"installer",
)
flush_logs()
send_full_log()
GLib.idle_add(self.show_finish_page, f"Installation Failed: {e}", False)
def on_next_clicked(self, button): def on_next_clicked(self, button):
# Logic before transition # Log navigation
current_page_name = self.page_ids[self.current_page_index] current_page_name = self.page_ids[self.current_page_index]
mode_str = "MOCK" if self.mock_mode else "PRODUCTION"
log_to_discord(
"NAVIGATION_NEXT",
f"Session: {self.session_id} - Page: {current_page_name} - Mode: {mode_str}",
"installer",
)
# Logic before transition
if current_page_name == "storage": if current_page_name == "storage":
# Pass selected disk to partitioning page
selected_disk = self.storage_page.get_selected_disk() selected_disk = self.storage_page.get_selected_disk()
if selected_disk: log_to_discord(
"SELECTION",
f"Session: {self.session_id} - Selected disk: {selected_disk}",
"installer",
)
self.partitioning_page.load_partitions(selected_disk) self.partitioning_page.load_partitions(selected_disk)
else:
# Optionally handle no disk selected (alert user)
pass
if self.current_page_index < len(self.page_ids) - 1: next_index = self.current_page_index + 1
self.current_page_index += 1
if current_page_name == "install_mode":
mode = self.install_mode_page.get_mode()
log_to_discord(
"SELECTION",
f"Session: {self.session_id} - Install mode: {mode}",
"installer",
)
if mode == "automatic":
# Skip partitioning page
next_index = self.page_ids.index("modules")
else:
# Go to partitioning page
next_index = self.page_ids.index("partitioning")
if current_page_name == "modules":
modules = self.modules_page.get_modules()
log_to_discord(
"SELECTION",
f"Session: {self.session_id} - Selected modules: {modules}",
"installer",
)
if current_page_name == "user":
# Prepare summary instead of installing immediately
disk = self.storage_page.get_selected_disk()
mode = self.install_mode_page.get_mode()
modules = self.modules_page.get_modules()
user_info = self.user_page.get_user_info()
# Log user info (without password for security)
log_to_discord(
"SELECTION",
f"Session: {self.session_id} - User: {user_info.get('username', 'N/A')}, Hostname: {user_info.get('hostname', 'N/A')}, Sudo: {user_info.get('allow_sudo', False)}",
"installer",
)
partitions_config = {}
if mode == "manual":
partitions_config = self.partitioning_page.get_config()
elif mode == "automatic":
partitions = calculate_auto_partitions(disk)
partitions_config = {"partitions": partitions}
# Update summary page
self.summary_page.update_summary(
disk_info=disk,
mode=mode,
partitions=partitions_config,
user_info=user_info,
modules=modules,
)
# Proceed to summary page (which is next_index after user)
if current_page_name == "summary":
# THIS IS THE REAL INSTALL TRIGGER
print("Install process triggered!")
disk = self.storage_page.get_selected_disk()
mode = self.install_mode_page.get_mode()
modules = self.modules_page.get_modules()
user_info = self.user_page.get_user_info()
# Log the install trigger with full config
log_to_discord(
"INSTALL_START",
f"Session: {self.session_id} - Disk: {disk} - Mode: {mode} - Modules: {modules} - Mock: {self.mock_mode}",
"installer",
)
if self.mock_mode:
print("!!! MOCK MODE ENABLED - NO CHANGES WILL BE MADE !!!")
print(f"Target Disk: {disk}")
print(f"Mode: {mode}")
print(f"Modules: {modules}")
print(f"User: {user_info}")
print("Simulation complete.")
log_to_discord(
"INSTALL_COMPLETE",
f"Session: {self.session_id} - Mock installation complete",
"installer",
)
flush_logs()
# Show success in UI even in mock
self.show_finish_page("Mock Installation Complete!")
else:
# Inhibit logout/suspend/idle
app = self.get_application()
self.inhibit_cookie = app.inhibit(
self,
Gtk.ApplicationInhibitFlags.LOGOUT | Gtk.ApplicationInhibitFlags.SUSPEND | Gtk.ApplicationInhibitFlags.IDLE,
"Installing Operating System"
)
self.show_progress_page("Installing Iridium OS...")
thread = threading.Thread(
target=self.run_installation,
args=(disk, mode, modules, user_info),
daemon=True,
)
thread.start()
return
if next_index < len(self.page_ids):
self.current_page_index = next_index
self.stack.set_visible_child_name(self.page_ids[self.current_page_index]) self.stack.set_visible_child_name(self.page_ids[self.current_page_index])
self.update_buttons() self.update_buttons()
else:
print("Install process triggered!")

52
run.sh
View File

@@ -1,5 +1,53 @@
#!/bin/bash #!/bin/bash
# Install icons to ~/.local/share/icons if not already installed
ICON_DIR="$HOME/.local/share/icons/hicolor/128x128/apps"
SYMBOLIC_DIR="$HOME/.local/share/icons/hicolor/symbolic/apps"
mkdir -p "$ICON_DIR"
mkdir -p "$SYMBOLIC_DIR"
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Copy main icon if not exists or if source is newer
if [ ! -f "$ICON_DIR/org.iridium.Installer.svg" ] || [ "${SCRIPT_DIR}/data/icons/org.iridium.Installer.svg" -nt "$ICON_DIR/org.iridium.Installer.svg" ]; then
cp "${SCRIPT_DIR}/data/icons/org.iridium.Installer.svg" "$ICON_DIR/"
echo "Installed 128x128 icon"
fi
# Copy symbolic icon if not exists or if source is newer
if [ ! -f "$SYMBOLIC_DIR/org.iridium.Installer-symbolic.svg" ] || [ "${SCRIPT_DIR}/data/icons/org.iridium.Installer-symbolic.svg" -nt "$SYMBOLIC_DIR/org.iridium.Installer-symbolic.svg" ]; then
cp "${SCRIPT_DIR}/data/icons/org.iridium.Installer-symbolic.svg" "$SYMBOLIC_DIR/org.iridium.Installer-symbolic.svg"
echo "Installed symbolic icon"
fi
# Update icon cache if gtk-update-icon-cache exists
if command -v gtk-update-icon-cache &> /dev/null; then
gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true
fi
# Check and install dependencies (Fedora/DNF)
if command -v dnf &> /dev/null; then
DEPENDENCIES="python3-gobject gtk4 libadwaita python3-requests gdisk dosfstools e2fsprogs parted grub2-tools openssl"
MISSING_DEPS=""
for dep in $DEPENDENCIES; do
if ! rpm -q $dep &> /dev/null; then
MISSING_DEPS="$MISSING_DEPS $dep"
fi
done
if [ -n "$MISSING_DEPS" ]; then
echo "Missing dependencies found:$MISSING_DEPS"
echo "Installing..."
sudo dnf install -y $MISSING_DEPS
fi
else
echo "Warning: 'dnf' not found. Automatic dependency installation skipped."
echo "Please ensure you have: python3-gobject gtk4 libadwaita python3-requests gdisk dosfstools e2fsprogs"
fi
export GSETTINGS_SCHEMA_DIR=. export GSETTINGS_SCHEMA_DIR=.
echo "Starting Iridium Installer..." python3 -m iridium_installer.main "$@"
python3 -m iridium_installer.main

28
run_vm_bios.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/bash
# Configuration
ISO_PATH="${1:-/home/n0va/.local/share/iridium-installer-vm/Fedora.iso}"
DISK_PATH="${2:-/home/n0va/.local/share/iridium-installer-vm/test-disk.qcow2}"
echo "Starting Iridium VM with BIOS (Legacy) support..."
echo "ISO: $ISO_PATH"
echo "Disk: $DISK_PATH"
# QEMU Command with BIOS
qemu-system-x86_64 \
-enable-kvm \
-m 8G \
-smp 2 \
-cpu host \
-drive file="$DISK_PATH",format=qcow2,if=virtio \
-device virtio-scsi-pci,id=scsi0 \
-drive file="$ISO_PATH",format=raw,if=none,id=cdrom,readonly=on \
-device scsi-cd,bus=scsi0.0,drive=cdrom \
-boot order=d \
-netdev user,id=net0 \
-device virtio-net-pci,netdev=net0 \
-vga virtio \
-display gtk,gl=on \
-monitor unix:$HOME/.local/share/iridium-installer-vm/monitor.sock,server,nowait \
-name "Iridium Installer Test VM (BIOS)" \
-loadvm clean-state

44
run_vm_uefi.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/bin/bash
# Configuration
ISO_PATH="${1:-/home/n0va/.local/share/iridium-installer-vm/Fedora.iso}"
DISK_PATH="${2:-/home/n0va/.local/share/iridium-installer-vm/test-disk.qcow2}"
OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
OVMF_VARS_LOCAL="/tmp/iridium_vm_vars.qcow2"
# Ensure OVMF vars exist in qcow2 format for snapshot support
if [ ! -f "$OVMF_VARS_LOCAL" ]; then
if [ -f "$OVMF_VARS_TEMPLATE" ]; then
echo "Creating UEFI vars file from template..."
qemu-img convert -f raw -O qcow2 "$OVMF_VARS_TEMPLATE" "$OVMF_VARS_LOCAL"
else
echo "Warning: OVMF VARS template not found at $OVMF_VARS_TEMPLATE"
# Fallback to creating an empty qcow2 if template is missing (not ideal but avoids crash)
qemu-img create -f qcow2 "$OVMF_VARS_LOCAL" 528K
fi
fi
echo "Starting Iridium VM with UEFI support..."
echo "ISO: $ISO_PATH"
echo "Disk: $DISK_PATH"
# QEMU Command with UEFI (OVMF) enabled
# We use format=qcow2 for pflash1 to allow snapshots (savevm)
qemu-system-x86_64 \
-enable-kvm \
-m 8G \
-smp 2 \
-cpu host \
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
-drive if=pflash,format=qcow2,file="$OVMF_VARS_LOCAL" \
-drive file="$DISK_PATH",format=qcow2,if=virtio \
-cdrom "$ISO_PATH" \
-boot once=d \
-netdev user,id=net0 \
-device virtio-net-pci,netdev=net0 \
-vga virtio \
-display gtk,gl=on \
-monitor unix:$HOME/.local/share/iridium-installer-vm/monitor.sock,server,nowait \
-name "Iridium Installer Test VM (UEFI)" \
-loadvm clean-state

19
take_snapshot.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
SOCKET="$HOME/.local/share/iridium-installer-vm/monitor.sock"
SNAPSHOT_NAME="${1:-clean-state}"
if [ ! -S "$SOCKET" ]; then
echo "Error: QEMU monitor socket not found at $SOCKET"
echo "Is the VM running?"
exit 1
fi
echo "Taking snapshot: $SNAPSHOT_NAME"
echo "savevm $SNAPSHOT_NAME" | nc -U "$SOCKET"
if [ $? -eq 0 ]; then
echo "Snapshot command sent successfully."
else
echo "Failed to send snapshot command. Ensure 'nc' (netcat) with -U support is installed."
fi