Compare commits

10 Commits

3 changed files with 159 additions and 113 deletions

View File

@@ -236,7 +236,8 @@ def mount_partitions(partition_info, mount_root="/mnt"):
# 2. Mount EFI (if exists) # 2. Mount EFI (if exists)
if partition_info.get("efi"): if partition_info.get("efi"):
efi_mount = os.path.join(mount_root, "boot/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) os.makedirs(efi_mount, exist_ok=True)
run_command(["mount", partition_info["efi"], efi_mount]) run_command(["mount", partition_info["efi"], efi_mount])
else: else:

View File

@@ -42,6 +42,12 @@ def run_command(cmd, check=True):
for line in stream: for line in stream:
line_clean = line.strip() line_clean = line.strip()
if line_clean: if line_clean:
# Progress parsing for rsync --info=progress2
if "%" in line_clean and any(x in line_clean for x in ["/", "speed", "to-check"]):
# This is likely a progress line, log it at INFO so it's visible but not spammy
# We only log every ~5th progress line to reduce noise if it's too fast
pass
log_level(line_clean) log_level(line_clean)
line_list.append(line) line_list.append(line)
@@ -50,7 +56,6 @@ def run_command(cmd, check=True):
t1 = threading.Thread( t1 = threading.Thread(
target=read_stream, args=(process.stdout, stdout_lines, logger.info) 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( t2 = threading.Thread(
target=read_stream, args=(process.stderr, stderr_lines, logger.info) target=read_stream, args=(process.stderr, stderr_lines, logger.info)
) )
@@ -68,7 +73,6 @@ def run_command(cmd, check=True):
if check and returncode != 0: if check and returncode != 0:
error_msg = f"Command failed: {' '.join(cmd)}\nExit Code: {returncode}\nStderr: {stderr_str}" 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) logger.error(error_msg)
raise subprocess.CalledProcessError( raise subprocess.CalledProcessError(
returncode, cmd, output=stdout_str, stderr=stderr_str returncode, cmd, output=stdout_str, stderr=stderr_str
@@ -126,104 +130,72 @@ def is_uefi():
def install_minimal_os(mount_root, releasever="43"): def install_minimal_os(mount_root, releasever="43"):
""" """
Installs minimal Fedora packages to mount_root. Installs the OS by rsyncing from the live environment (fully offline).
""" """
logger.info(f"Installing minimal Fedora {releasever} to {mount_root}...") logger.info(f"Installing Iridium OS to {mount_root} via rsync...")
log_os_install("INSTALL", "start", f"Target: {mount_root}, Release: {releasever}") log_os_install("INSTALL", "start", f"Target: {mount_root}")
uefi = is_uefi() uefi = is_uefi()
packages = [ # Exclude list for rsync to avoid copying pseudo-filesystems and temporary data
"basesystem", excludes = [
"bash", "/dev/*",
"coreutils", "/proc/*",
"kernel", "/sys/*",
"systemd", "/tmp/*",
"dnf", "/run/*",
"shadow-utils", "/mnt/*",
"util-linux", "/media/*",
"passwd", "/lost+found",
"rootfiles", "/home/*/.gvfs",
"vim-minimal", "/home/*/.cache",
"grub2-tools", "/home/*/.local/share/Trash",
"grubby", "/var/lib/dnf/*",
"/var/cache/dnf/*",
"/etc/fstab",
"/etc/hostname",
"/boot", # We handle boot separately (including the dir itself)
# Avoid copying the installer data itself if it's in home
"domek_na_skale",
] ]
if uefi: exclude_args = [f"--exclude={ex}" for ex in excludes]
packages += ["grub2-efi-x64", "grub2-efi-x64-modules", "shim-x64", "efibootmgr"]
else:
packages += ["grub2-pc"]
# Offline installation logic # 1. Main Root Sync
possible_repos = [ # We use -a (archive), -H (hard links), -A (acls), -X (xattrs), -v (verbose), -x (one file system)
"/run/install/repo", # --info=progress2 gives a nice summary for logging
"/run/install/source", cmd = ["rsync", "-aHAXvx", "--info=progress2"] + exclude_args + ["/", mount_root]
"/mnt/install/repo",
"/run/initramfs/live",
"/run/initramfs/isoscan",
]
iso_repo = None logger.info("Starting main rsync operation...")
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) run_command(cmd)
logger.info("Base system installation complete.") # 2. Boot Sync
# If UEFI, /boot is likely FAT32 which doesn't support symlinks or xattrs
logger.info(f"Syncing /boot (UEFI: {uefi})...")
boot_dst = os.path.join(mount_root, "boot/")
os.makedirs(boot_dst, exist_ok=True)
if uefi:
# FAT32 friendly: follow links (-L), recursive (-r), preserve times (-t)
# We skip ACLs, xattrs, and hardlinks as they are not supported
boot_cmd = ["rsync", "-rtvL", "--info=progress2", "/boot/", boot_dst]
else:
# BIOS (ext4): standard archive is fine
boot_cmd = ["rsync", "-aHAXvx", "--info=progress2", "/boot/", boot_dst]
run_command(boot_cmd)
# Ensure essential directories exist
for d in ["dev", "proc", "sys", "run", "mnt", "tmp"]:
os.makedirs(os.path.join(mount_root, d), exist_ok=True)
logger.info("Base system rsync complete.")
log_os_install("INSTALL", "complete", f"Installed to {mount_root}") log_os_install("INSTALL", "complete", f"Installed to {mount_root}")
def configure_system(mount_root, partition_info, user_info=None, disk_device=None): def configure_system(mount_root, partition_info, user_info=None, disk_device=None):
""" """
Basic configuration: fstab, grub2, and user creation. Basic configuration: fstab, bootloader, and user creation.
""" """
logger.info("Configuring system...") logger.info("Configuring system...")
log_os_install("CONFIGURE", "start", f"Configuring system in {mount_root}") log_os_install("CONFIGURE", "start", f"Configuring system in {mount_root}")
@@ -241,7 +213,8 @@ def configure_system(mount_root, partition_info, user_info=None, disk_device=Non
if uefi and partition_info.get("efi"): if uefi and partition_info.get("efi"):
efi_uuid = get_uuid(partition_info["efi"]) efi_uuid = get_uuid(partition_info["efi"])
fstab_lines.append(f"UUID={efi_uuid} /boot/efi vfat defaults 0 2") # 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"): if partition_info.get("swap"):
swap_uuid = get_uuid(partition_info["swap"]) swap_uuid = get_uuid(partition_info["swap"])
@@ -251,9 +224,9 @@ def configure_system(mount_root, partition_info, user_info=None, disk_device=Non
with open(os.path.join(mount_root, "etc/fstab"), "w") as f: with open(os.path.join(mount_root, "etc/fstab"), "w") as f:
f.write("\n".join(fstab_lines) + "\n") f.write("\n".join(fstab_lines) + "\n")
# Ensure EFI is mounted for GRUB installation # Ensure EFI is mounted for bootloader installation if UEFI
if uefi and partition_info.get("efi"): if uefi and partition_info.get("efi"):
efi_target = os.path.join(mount_root, "boot/efi") efi_target = os.path.join(mount_root, "boot")
os.makedirs(efi_target, exist_ok=True) os.makedirs(efi_target, exist_ok=True)
# Check if already mounted # Check if already mounted
res = subprocess.run(["mount"], capture_output=True, text=True) res = subprocess.run(["mount"], capture_output=True, text=True)
@@ -264,6 +237,15 @@ def configure_system(mount_root, partition_info, user_info=None, disk_device=Non
# 2. Configure User # 2. Configure User
if user_info: if user_info:
logger.info(f"Creating user {user_info['username']}...") logger.info(f"Creating user {user_info['username']}...")
# Since we rsync from live, we might have a 'liveuser' or similar.
# We should probably clear /home and /etc/passwd entries that are non-system
# but for now, let's just make sure we can create the new one.
try:
# Remove liveuser if it exists (common in Fedora live)
run_command(["chroot", mount_root, "userdel", "-r", "liveuser"], check=False)
except: pass
run_command(["chroot", mount_root, "useradd", "-m", "-G", "wheel", user_info["username"]]) run_command(["chroot", mount_root, "useradd", "-m", "-G", "wheel", user_info["username"]])
# Set hostname # Set hostname
@@ -284,35 +266,66 @@ def configure_system(mount_root, partition_info, user_info=None, disk_device=Non
except Exception as e: except Exception as e:
logger.error(f"Failed to set passwords: {e}") logger.error(f"Failed to set passwords: {e}")
# 3. Configure GRUB2 # 3. Configure Bootloader
logger.info("Configuring GRUB2...") if uefi:
logger.info("Configuring systemd-boot...")
# Remove any copied machine-id to ensure a unique one is generated
mid_path = os.path.join(mount_root, "etc/machine-id")
if os.path.exists(mid_path):
os.remove(mid_path)
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")
# Cleanup existing systemd-boot files to ensure a fresh install since --force is not supported
for d in ["loader", "EFI/systemd", "EFI/BOOT"]:
path = os.path.join(mount_root, "boot", d)
if os.path.exists(path):
import shutil
if os.path.isdir(path):
shutil.rmtree(path)
else:
os.remove(path)
# Initialize systemd-boot
run_command(["chroot", mount_root, "bootctl", "install", "--path=/boot"])
# Sync kernels and generate BLS entries
# Since we rsync'd, kernels are in /lib/modules and /boot
# We use kernel-install to make sure they are properly set up for systemd-boot
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"Setting up boot entries for kernel {kver}...")
# Ensure initramfs exists in /boot (rsync should have copied it, but let's be sure)
initrd_path = f"/boot/initramfs-{kver}.img"
vmlinuz_path = f"/boot/vmlinuz-{kver}"
if not os.path.exists(os.path.join(mount_root, initrd_path.lstrip("/"))):
logger.info(f"Generating initramfs for {kver}...")
run_command(["chroot", mount_root, "dracut", "--force", initrd_path, kver])
# kernel-install add <version> <image> [initrd]
# On Fedora, kernel-install add will populate /boot/<machine-id>/<version>/
if os.path.exists(os.path.join(mount_root, vmlinuz_path.lstrip("/"))):
run_command(["chroot", mount_root, "kernel-install", "add", kver, vmlinuz_path])
else:
logger.info("Configuring GRUB2 (BIOS)...")
# Ensure /etc/default/grub exists # Ensure /etc/default/grub exists
grub_default = os.path.join(mount_root, "etc/default/grub") grub_default = os.path.join(mount_root, "etc/default/grub")
if not os.path.exists(grub_default): if not os.path.exists(grub_default):
with open(grub_default, "w") as f: 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') 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 uefi:
efi_dir = "/boot/efi"
# Re-verify mount inside chroot if possible, but we already mounted it outside
logger.info("Installing GRUB2 for UEFI...")
# Using --removable can help on some systems that don't like efibootmgr entries
# but for now let's stick to standard and add --recheck
run_command(["chroot", mount_root, "grub2-install", "--target=x86_64-efi", f"--efi-directory={efi_dir}", "--bootloader-id=iridium", "--recheck"])
config_path = "/boot/efi/EFI/iridium/grub.cfg"
run_command(["chroot", mount_root, "grub2-mkconfig", "-o", config_path])
# Fedora compatibility link
fedora_dir = os.path.join(mount_root, "boot/efi/EFI/fedora")
os.makedirs(fedora_dir, exist_ok=True)
with open(os.path.join(fedora_dir, "grub.cfg"), "w") as f:
f.write(f"configfile /EFI/iridium/grub.cfg\n")
else:
if not disk_device: if not disk_device:
# Try to guess disk device from root partition
disk_device = partition_info["root"].rstrip("0123456789") disk_device = partition_info["root"].rstrip("0123456789")
if disk_device.endswith("p"): disk_device = disk_device[:-1] if disk_device.endswith("p"): disk_device = disk_device[:-1]
@@ -323,4 +336,4 @@ def configure_system(mount_root, partition_info, user_info=None, disk_device=Non
run_command(["sync"]) run_command(["sync"])
logger.info("System configuration complete.") logger.info("System configuration complete.")
log_os_install("CONFIGURE", "complete", "GRUB2 and user configured successfully") log_os_install("CONFIGURE", "complete", "Bootloader and user configured successfully")

View File

@@ -182,6 +182,13 @@ class InstallerWindow(Adw.ApplicationWindow):
spinner.start() spinner.start()
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
self.progress_bar = Gtk.ProgressBar()
self.progress_bar.set_show_text(True)
self.progress_bar.set_margin_start(24)
self.progress_bar.set_margin_end(24)
box.append(self.progress_bar)
box.append(spinner) box.append(spinner)
# Log view # Log view
@@ -226,6 +233,31 @@ class InstallerWindow(Adw.ApplicationWindow):
mark = self.log_buffer.create_mark("end", end_iter, False) mark = self.log_buffer.create_mark("end", end_iter, False)
self.log_view.scroll_to_mark(mark, 0.0, True, 0.0, 1.0) self.log_view.scroll_to_mark(mark, 0.0, True, 0.0, 1.0)
# Update progress bar based on keywords or rsync progress
msg_lower = msg.lower()
if "step 1:" in msg_lower:
self.progress_bar.set_fraction(0.1)
self.progress_bar.set_text("Partitioning...")
elif "step 2:" in msg_lower:
self.progress_bar.set_fraction(0.2)
self.progress_bar.set_text("Mounting...")
elif "step 3:" in msg_lower:
self.progress_bar.set_fraction(0.3)
self.progress_bar.set_text("Installing OS...")
elif "step 4:" in msg_lower:
self.progress_bar.set_fraction(0.9)
self.progress_bar.set_text("Configuring Bootloader...")
elif "%" in msg and "to-check" in msg:
# Parse rsync progress (e.g., "1,234,567 80% 10.00MB/s 0:00:05")
import re
match = re.search(r"(\d+)%", msg)
if match:
percentage = int(match.group(1))
# Map 0-100% of rsync to 0.3-0.8 of total progress
fraction = 0.3 + (percentage / 100.0) * 0.5
self.progress_bar.set_fraction(fraction)
self.progress_bar.set_text(f"Syncing files... {percentage}%")
def show_finish_page(self, message, success=True): def show_finish_page(self, message, success=True):
if self.log_handler: if self.log_handler:
logging.getLogger().removeHandler(self.log_handler) logging.getLogger().removeHandler(self.log_handler)