From a57b037a91a7efe6b68e4accfb7fa0835d4900a6 Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Thu, 26 Mar 2026 23:35:01 +0300 Subject: [PATCH] feat(installer): add 'Install to disk' in Tools submenu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copies the live system to a local disk via unsquashfs — no debootstrap, no network required. Supports UEFI (GPT+EFI) and BIOS (MBR) layouts. ISO: - Add squashfs-tools, parted, grub-pc, grub-efi-amd64 to package list - New overlay script bee-install: partitions, formats, unsquashfs, writes fstab, runs grub-install+update-grub in chroot Go TUI: - Settings → Tools submenu (Install to disk, Check tools) - Disk picker screen: lists non-USB, non-boot disks via lsblk - Confirm screen warns about data loss - Runs with live progress tail of /tmp/bee-install.log - platform/install.go: ListInstallDisks, InstallToDisk, findLiveBootDevice Co-Authored-By: Claude Sonnet 4.6 --- audit/internal/app/app.go | 39 ++-- audit/internal/platform/install.go | 105 ++++++++++ audit/internal/tui/forms.go | 6 +- audit/internal/tui/messages.go | 9 + audit/internal/tui/sat_progress.go | 20 ++ audit/internal/tui/screen_settings.go | 15 +- audit/internal/tui/screen_tools.go | 108 ++++++++++ audit/internal/tui/types.go | 24 +++ audit/internal/tui/update.go | 54 +++++ audit/internal/tui/view.go | 4 + .../config/package-lists/bee.list.chroot | 6 + iso/overlay/usr/local/bin/bee-install | 189 ++++++++++++++++++ 12 files changed, 555 insertions(+), 24 deletions(-) create mode 100644 audit/internal/platform/install.go create mode 100644 audit/internal/tui/screen_tools.go create mode 100755 iso/overlay/usr/local/bin/bee-install diff --git a/audit/internal/app/app.go b/audit/internal/app/app.go index a2c6a65..6a14e7f 100644 --- a/audit/internal/app/app.go +++ b/audit/internal/app/app.go @@ -33,12 +33,13 @@ var ( ) type App struct { - network networkManager - services serviceManager - exports exportManager - tools toolManager - sat satRunner - runtime runtimeChecker + network networkManager + services serviceManager + exports exportManager + tools toolManager + sat satRunner + runtime runtimeChecker + installer installer } type ActionResult struct { @@ -70,6 +71,11 @@ type toolManager interface { CheckTools(names []string) []platform.ToolStatus } +type installer interface { + ListInstallDisks() ([]platform.InstallDisk, error) + InstallToDisk(ctx context.Context, device string, logFile string) error +} + type satRunner interface { RunNvidiaAcceptancePack(baseDir string) (string, error) RunNvidiaAcceptancePackWithOptions(ctx context.Context, baseDir string, diagLevel int, gpuIndices []int) (string, error) @@ -91,12 +97,13 @@ type runtimeChecker interface { func New(platform *platform.System) *App { return &App{ - network: platform, - services: platform, - exports: platform, - tools: platform, - sat: platform, - runtime: platform, + network: platform, + services: platform, + exports: platform, + tools: platform, + sat: platform, + runtime: platform, + installer: platform, } } @@ -1003,3 +1010,11 @@ func firstNonEmpty(values ...string) string { } return "" } + +func (a *App) ListInstallDisks() ([]platform.InstallDisk, error) { + return a.installer.ListInstallDisks() +} + +func (a *App) InstallToDisk(ctx context.Context, device string, logFile string) error { + return a.installer.InstallToDisk(ctx, device, logFile) +} diff --git a/audit/internal/platform/install.go b/audit/internal/platform/install.go new file mode 100644 index 0000000..5f14c0a --- /dev/null +++ b/audit/internal/platform/install.go @@ -0,0 +1,105 @@ +package platform + +import ( + "context" + "fmt" + "os/exec" + "strconv" + "strings" +) + +// InstallDisk describes a candidate disk for installation. +type InstallDisk struct { + Device string // e.g. /dev/sda + Model string + Size string // human-readable, e.g. "500G" +} + +// ListInstallDisks returns block devices suitable for installation. +// Excludes USB drives and the current live boot medium. +func (s *System) ListInstallDisks() ([]InstallDisk, error) { + out, err := exec.Command("lsblk", "-dn", "-o", "NAME,MODEL,SIZE,TYPE,TRAN").Output() + if err != nil { + return nil, fmt.Errorf("lsblk: %w", err) + } + + bootDev := findLiveBootDevice() + + var disks []InstallDisk + for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { + fields := strings.Fields(line) + // NAME MODEL SIZE TYPE TRAN — model may have spaces so we parse from end + if len(fields) < 4 { + continue + } + // Last field: TRAN, second-to-last: TYPE, third-to-last: SIZE + tran := fields[len(fields)-1] + typ := fields[len(fields)-2] + size := fields[len(fields)-3] + name := fields[0] + model := strings.Join(fields[1:len(fields)-3], " ") + + if typ != "disk" { + continue + } + if strings.EqualFold(tran, "usb") { + continue + } + + device := "/dev/" + name + if device == bootDev { + continue + } + + disks = append(disks, InstallDisk{ + Device: device, + Model: strings.TrimSpace(model), + Size: size, + }) + } + return disks, nil +} + +// findLiveBootDevice returns the block device backing /run/live/medium (if any). +func findLiveBootDevice() string { + out, err := exec.Command("findmnt", "-n", "-o", "SOURCE", "/run/live/medium").Output() + if err != nil { + return "" + } + src := strings.TrimSpace(string(out)) + if src == "" { + return "" + } + // Strip partition suffix to get the whole disk device. + // e.g. /dev/sdb1 → /dev/sdb, /dev/nvme0n1p1 → /dev/nvme0n1 + out2, err := exec.Command("lsblk", "-no", "PKNAME", src).Output() + if err != nil || strings.TrimSpace(string(out2)) == "" { + return src + } + return "/dev/" + strings.TrimSpace(string(out2)) +} + +// InstallToDisk runs bee-install and streams output to logFile. +// The context can be used to cancel. +func (s *System) InstallToDisk(ctx context.Context, device string, logFile string) error { + cmd := exec.CommandContext(ctx, "bee-install", device, logFile) + return cmd.Run() +} + +// InstallLogPath returns the default install log path for a given device. +func InstallLogPath(device string) string { + safe := strings.NewReplacer("/", "_", " ", "_").Replace(device) + return "/tmp/bee-install" + safe + ".log" +} + +// DiskLabel returns a display label for a disk. +func (d InstallDisk) Label() string { + model := d.Model + if model == "" { + model = "Unknown" + } + sizeBytes, err := strconv.ParseInt(strings.TrimSuffix(d.Size, "B"), 10, 64) + _ = sizeBytes + _ = err + return fmt.Sprintf("%s %s %s", d.Device, d.Size, model) +} diff --git a/audit/internal/tui/forms.go b/audit/internal/tui/forms.go index 3ecaf9e..549c919 100644 --- a/audit/internal/tui/forms.go +++ b/audit/internal/tui/forms.go @@ -141,7 +141,9 @@ func (m model) updateConfirm(msg tea.KeyMsg) (tea.Model, tea.Cmd) { ) case actionRunFanStress: return m.startGPUStressTest() - case actionRunNCCLTests: + case actionInstallToDisk: + return m.startInstall() + case actionRunNCCLTests: m.busy = true m.busyTitle = "NCCL bandwidth test" ctx, cancel := context.WithCancel(context.Background()) @@ -165,6 +167,8 @@ func (m model) confirmCancelTarget() screen { return screenHealthCheck case actionRunFanStress, actionRunNCCLTests: return screenBurnInTests + case actionInstallToDisk: + return screenInstallDiskPick default: return screenMain } diff --git a/audit/internal/tui/messages.go b/audit/internal/tui/messages.go index b599dd9..c1bb430 100644 --- a/audit/internal/tui/messages.go +++ b/audit/internal/tui/messages.go @@ -50,3 +50,12 @@ type gpuLiveTickMsg struct { rows []platform.GPUMetricRow indices []int } + +type installDisksMsg struct { + disks []platform.InstallDisk + err error +} + +type installDoneMsg struct { + err error +} diff --git a/audit/internal/tui/sat_progress.go b/audit/internal/tui/sat_progress.go index c0c71a3..27d0d5f 100644 --- a/audit/internal/tui/sat_progress.go +++ b/audit/internal/tui/sat_progress.go @@ -123,6 +123,26 @@ func cleanSATStepName(name string) string { return name } +// pollInstallProgress tails the install log file and returns recent lines as progress. +func pollInstallProgress(logFile string) tea.Cmd { + return tea.Tick(500*time.Millisecond, func(_ time.Time) tea.Msg { + return satProgressMsg{lines: readInstallProgressLines(logFile)} + }) +} + +func readInstallProgressLines(logFile string) []string { + raw, err := os.ReadFile(logFile) + if err != nil { + return nil + } + lines := strings.Split(strings.TrimSpace(string(raw)), "\n") + // Show last 12 lines + if len(lines) > 12 { + lines = lines[len(lines)-12:] + } + return lines +} + func fmtDurMs(ms int) string { if ms < 1000 { return fmt.Sprintf("%dms", ms) diff --git a/audit/internal/tui/screen_settings.go b/audit/internal/tui/screen_settings.go index 021bd22..b068f82 100644 --- a/audit/internal/tui/screen_settings.go +++ b/audit/internal/tui/screen_settings.go @@ -44,17 +44,10 @@ func (m model) handleSettingsMenu() (tea.Model, tea.Cmd) { result := m.app.AuditLogTailResult() return resultMsg{title: result.Title, body: result.Body, back: screenSettings} } - case 6: // Check tools - m.busy = true - m.busyTitle = "Check tools" - return m, func() tea.Msg { - result := m.app.ToolCheckResult([]string{ - "dmidecode", "smartctl", "nvme", "ipmitool", "lspci", - "ethtool", "bee", "nvidia-smi", "bee-gpu-stress", - "memtester", "dhclient", "lsblk", "mount", - }) - return resultMsg{title: result.Title, body: result.Body, back: screenSettings} - } + case 6: // Tools + m.screen = screenTools + m.cursor = 0 + return m, nil case 7: // Back m.screen = screenMain m.cursor = 0 diff --git a/audit/internal/tui/screen_tools.go b/audit/internal/tui/screen_tools.go new file mode 100644 index 0000000..b684241 --- /dev/null +++ b/audit/internal/tui/screen_tools.go @@ -0,0 +1,108 @@ +package tui + +import ( + "context" + "fmt" + "strings" + "time" + + "bee/audit/internal/platform" + + tea "github.com/charmbracelet/bubbletea" +) + +// handleToolsMenu handles the Tools submenu selection. +func (m model) handleToolsMenu() (tea.Model, tea.Cmd) { + switch m.cursor { + case 0: // Install to disk + m.busy = true + m.busyTitle = "Scanning disks" + return m, func() tea.Msg { + disks, err := m.app.ListInstallDisks() + return installDisksMsg{disks: disks, err: err} + } + case 1: // Check tools + m.busy = true + m.busyTitle = "Check tools" + return m, func() tea.Msg { + result := m.app.ToolCheckResult([]string{ + "dmidecode", "smartctl", "nvme", "ipmitool", "lspci", + "ethtool", "bee", "nvidia-smi", "bee-gpu-stress", + "memtester", "dhclient", "lsblk", "mount", + "unsquashfs", "parted", "grub-install", "bee-install", + "all_reduce_perf", "dcgmi", + }) + return resultMsg{title: result.Title, body: result.Body, back: screenTools} + } + case 2: // Back + m.screen = screenSettings + m.cursor = 0 + return m, nil + } + return m, nil +} + +// handleInstallDiskPickMenu handles disk selection for installation. +func (m model) handleInstallDiskPickMenu() (tea.Model, tea.Cmd) { + if m.cursor >= len(m.installDisks) { + return m, nil + } + m.selectedDisk = m.installDisks[m.cursor].Device + m.pendingAction = actionInstallToDisk + m.screen = screenConfirm + m.cursor = 0 + return m, nil +} + +// startInstall launches the bee-install script and polls its log for progress. +func (m model) startInstall() (tea.Model, tea.Cmd) { + device := m.selectedDisk + logFile := DefaultInstallLogFile + + ctx, cancel := context.WithCancel(context.Background()) + m.installCancel = cancel + m.installSince = time.Now() + m.busy = true + m.busyTitle = "Install to disk: " + device + m.progressLines = nil + + installCmd := func() tea.Msg { + err := m.app.InstallToDisk(ctx, device, logFile) + return installDoneMsg{err: err} + } + + return m, tea.Batch(installCmd, pollInstallProgress(logFile)) +} + +func renderInstallDiskPick(m model) string { + var b strings.Builder + fmt.Fprintln(&b, "INSTALL TO DISK") + fmt.Fprintln(&b) + fmt.Fprintln(&b, " WARNING: the selected disk will be completely WIPED.") + fmt.Fprintln(&b) + fmt.Fprintln(&b, " Available disks (USB and boot media excluded):") + fmt.Fprintln(&b) + if len(m.installDisks) == 0 { + fmt.Fprintln(&b, " (no suitable disks found)") + } else { + for i, d := range m.installDisks { + pfx := " " + if m.cursor == i { + pfx = "> " + } + fmt.Fprintf(&b, "%s%s\n", pfx, diskLabel(d)) + } + } + fmt.Fprintln(&b) + fmt.Fprintln(&b, "─────────────────────────────────────────────────────────────────") + fmt.Fprint(&b, "[↑/↓] move [enter] select [esc] back [ctrl+c] quit") + return b.String() +} + +func diskLabel(d platform.InstallDisk) string { + model := d.Model + if model == "" { + model = "Unknown" + } + return fmt.Sprintf("%-12s %-8s %s", d.Device, d.Size, model) +} diff --git a/audit/internal/tui/types.go b/audit/internal/tui/types.go index ce22517..8f8fc37 100644 --- a/audit/internal/tui/types.go +++ b/audit/internal/tui/types.go @@ -11,6 +11,9 @@ import ( tea "github.com/charmbracelet/bubbletea" ) +// DefaultInstallLogFile is where bee-install writes its progress log. +const DefaultInstallLogFile = "/tmp/bee-install.log" + type screen string const ( @@ -29,6 +32,8 @@ const ( screenNvidiaSATSetup screen = "nvidia_sat_setup" screenNvidiaSATRunning screen = "nvidia_sat_running" screenGPUStressRunning screen = "gpu_stress_running" + screenTools screen = "tools" + screenInstallDiskPick screen = "install_disk_pick" ) type actionKind string @@ -45,6 +50,7 @@ const ( actionRunAMDGPUSAT actionKind = "run_amd_gpu_sat" actionRunFanStress actionKind = "run_fan_stress" actionRunNCCLTests actionKind = "run_nccl_tests" + actionInstallToDisk actionKind = "install_to_disk" ) type model struct { @@ -62,13 +68,16 @@ type model struct { settingsMenu []string networkMenu []string serviceMenu []string + toolsMenu []string services []string interfaces []platform.InterfaceInfo targets []platform.RemovableTarget + installDisks []platform.InstallDisk selectedService string selectedIface string selectedTarget *platform.RemovableTarget + selectedDisk string pendingAction actionKind formFields []formField @@ -102,6 +111,10 @@ type model struct { // NCCL tests running ncclCancel func() + // Install to disk + installCancel func() + installSince time.Time + // GPU Platform Stress Test running gpuStressCancel func() gpuStressAborted bool @@ -152,6 +165,11 @@ func newModel(application *app.App, runtimeMode runtimeenv.Mode) model { "Run self-check", "Runtime issues", "Audit logs", + "Tools", + "Back", + }, + toolsMenu: []string{ + "Install to disk", "Check tools", "Back", }, @@ -206,6 +224,12 @@ func (m model) confirmBody() (string, string) { return "CPU test", "Run stress-ng? Mode: " + modes[m.hcMode] case actionRunAMDGPUSAT: return "AMD GPU test", "Run AMD GPU diagnostic pack (rocm-smi)?" + case actionInstallToDisk: + dev := m.selectedDisk + if dev == "" { + dev = "unknown" + } + return "Install to disk", "WARNING: " + dev + " will be completely WIPED.\n\nAll data on the disk will be lost!\n\nInstall bee live system to " + dev + "?" case actionRunNCCLTests: return "NCCL bandwidth test", "Run all_reduce_perf across all GPUs?\n\nMeasures collective bandwidth over NVLink/PCIe.\nRequires 2+ GPUs for meaningful results." case actionRunFanStress: diff --git a/audit/internal/tui/update.go b/audit/internal/tui/update.go index 3d64096..7d4d6a9 100644 --- a/audit/internal/tui/update.go +++ b/audit/internal/tui/update.go @@ -33,6 +33,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, pollSATProgress(m.progressPrefix, m.progressSince) } + if m.busy && m.installCancel != nil { + if len(msg.lines) > 0 { + m.progressLines = msg.lines + } + return m, pollInstallProgress(DefaultInstallLogFile) + } return m, nil case snapshotMsg: m.banner = msg.banner @@ -112,6 +118,44 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.screen = screenExportTargets m.cursor = 0 return m, m.refreshSnapshotCmd() + case installDisksMsg: + m.busy = false + m.busyTitle = "" + if msg.err != nil { + m.title = "Install to disk" + m.body = msg.err.Error() + m.prevScreen = screenTools + m.screen = screenOutput + return m, m.refreshSnapshotCmd() + } + if len(msg.disks) == 0 { + m.title = "Install to disk" + m.body = "No suitable disks found.\n\nOnly non-USB, non-boot disks are shown.\nAttach a target disk and try again." + m.prevScreen = screenTools + m.screen = screenOutput + return m, m.refreshSnapshotCmd() + } + m.installDisks = msg.disks + m.screen = screenInstallDiskPick + m.cursor = 0 + return m, m.refreshSnapshotCmd() + case installDoneMsg: + if m.installCancel != nil { + m.installCancel() + m.installCancel = nil + } + m.busy = false + m.busyTitle = "" + m.progressLines = nil + m.prevScreen = screenTools + m.screen = screenOutput + m.title = "Install to disk" + if msg.err != nil { + m.body = fmt.Sprintf("Installation FAILED.\n\nLog: %s\n\nERROR: %v", DefaultInstallLogFile, msg.err) + } else { + m.body = fmt.Sprintf("Installation complete.\n\nRemove the ISO and reboot to start the installed system.\n\nLog: %s", DefaultInstallLogFile) + } + return m, m.refreshSnapshotCmd() case nvtopClosedMsg: return m, nil case gpuStressDoneMsg: @@ -204,6 +248,10 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m.updateMenu(msg, len(m.targets), m.handleExportTargetsMenu) case screenInterfacePick: return m.updateMenu(msg, len(m.interfaces), m.handleInterfacePickMenu) + case screenTools: + return m.updateMenu(msg, len(m.toolsMenu), m.handleToolsMenu) + case screenInstallDiskPick: + return m.updateMenu(msg, len(m.installDisks), m.handleInstallDiskPickMenu) case screenOutput: switch msg.String() { case "esc", "enter", "q": @@ -300,6 +348,12 @@ func (m model) updateMenu(msg tea.KeyMsg, size int, onEnter func() (tea.Model, t case screenInterfacePick: m.screen = screenNetwork m.cursor = 0 + case screenTools: + m.screen = screenSettings + m.cursor = 0 + case screenInstallDiskPick: + m.screen = screenTools + m.cursor = 0 } case "q", "ctrl+c": return m, tea.Quit diff --git a/audit/internal/tui/view.go b/audit/internal/tui/view.go index 487ee03..8f90ee6 100644 --- a/audit/internal/tui/view.go +++ b/audit/internal/tui/view.go @@ -76,6 +76,10 @@ func (m model) View() string { ) case screenInterfacePick: body = renderMenu("Interfaces", "Select interface", renderInterfaceItems(m.interfaces), m.cursor) + case screenTools: + body = renderMenu("Tools", "Select action", m.toolsMenu, m.cursor) + case screenInstallDiskPick: + body = renderInstallDiskPick(m) case screenStaticForm: body = renderForm("Static IPv4: "+m.selectedIface, m.formFields, m.formIndex) case screenConfirm: diff --git a/iso/builder/config/package-lists/bee.list.chroot b/iso/builder/config/package-lists/bee.list.chroot index 654f796..de650ab 100644 --- a/iso/builder/config/package-lists/bee.list.chroot +++ b/iso/builder/config/package-lists/bee.list.chroot @@ -18,6 +18,12 @@ qemu-guest-agent # SSH openssh-server +# Disk installer +squashfs-tools +parted +grub-pc +grub-efi-amd64 + # Filesystem support for USB export targets exfatprogs exfat-fuse diff --git a/iso/overlay/usr/local/bin/bee-install b/iso/overlay/usr/local/bin/bee-install new file mode 100755 index 0000000..7a4ebb6 --- /dev/null +++ b/iso/overlay/usr/local/bin/bee-install @@ -0,0 +1,189 @@ +#!/bin/bash +# bee-install — install the live system to a local disk. +# +# Usage: bee-install [logfile] +# device — target block device, e.g. /dev/sda (will be WIPED) +# logfile — optional path to write progress log (default: /tmp/bee-install.log) +# +# Layout (UEFI): GPT, /dev/sdX1=EFI 512MB vfat, /dev/sdX2=root ext4 +# Layout (BIOS): MBR, /dev/sdX1=root ext4 +# +# Squashfs source: /run/live/medium/live/filesystem.squashfs + +set -euo pipefail + +DEVICE="${1:-}" +LOGFILE="${2:-/tmp/bee-install.log}" + +if [ -z "$DEVICE" ]; then + echo "Usage: bee-install [logfile]" >&2 + exit 1 +fi +if [ ! -b "$DEVICE" ]; then + echo "ERROR: $DEVICE is not a block device" >&2 + exit 1 +fi + +SQUASHFS="/run/live/medium/live/filesystem.squashfs" +if [ ! -f "$SQUASHFS" ]; then + echo "ERROR: squashfs not found at $SQUASHFS" >&2 + exit 1 +fi + +MOUNT_ROOT="/mnt/bee-install-root" + +# ------------------------------------------------------------------ +log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOGFILE"; } +die() { log "ERROR: $*"; exit 1; } + +# ------------------------------------------------------------------ +# Detect UEFI +if [ -d /sys/firmware/efi ]; then + UEFI=1 + log "Boot mode: UEFI" +else + UEFI=0 + log "Boot mode: BIOS/legacy" +fi + +# Determine partition names (nvme uses p-suffix: nvme0n1p1) +if echo "$DEVICE" | grep -qE 'nvme|mmcblk'; then + PART_PREFIX="${DEVICE}p" +else + PART_PREFIX="${DEVICE}" +fi + +if [ "$UEFI" = "1" ]; then + PART_EFI="${PART_PREFIX}1" + PART_ROOT="${PART_PREFIX}2" +else + PART_ROOT="${PART_PREFIX}1" +fi + +# ------------------------------------------------------------------ +log "=== BEE DISK INSTALLER ===" +log "Target device : $DEVICE" +log "Root partition: $PART_ROOT" +[ "$UEFI" = "1" ] && log "EFI partition : $PART_EFI" +log "Squashfs : $SQUASHFS ($(du -sh "$SQUASHFS" | cut -f1))" +log "Log : $LOGFILE" +log "" + +# ------------------------------------------------------------------ +log "--- Step 1/7: Unmounting target device ---" +# Unmount any partitions on target device +for part in "${DEVICE}"* ; do + if [ "$part" = "$DEVICE" ]; then continue; fi + if mountpoint -q "$part" 2>/dev/null; then + log " umount $part" + umount "$part" || true + fi +done +# Also unmount our mount point if leftover +umount "${MOUNT_ROOT}" 2>/dev/null || true +umount "${MOUNT_ROOT}/boot/efi" 2>/dev/null || true + +# ------------------------------------------------------------------ +log "--- Step 2/7: Partitioning $DEVICE ---" +if [ "$UEFI" = "1" ]; then + parted -s "$DEVICE" mklabel gpt + parted -s "$DEVICE" mkpart EFI fat32 1MiB 513MiB + parted -s "$DEVICE" set 1 esp on + parted -s "$DEVICE" mkpart root ext4 513MiB 100% +else + parted -s "$DEVICE" mklabel msdos + parted -s "$DEVICE" mkpart primary ext4 1MiB 100% + parted -s "$DEVICE" set 1 boot on +fi +# Wait for kernel to see new partitions +sleep 1 +partprobe "$DEVICE" 2>/dev/null || true +sleep 1 +log " Partitioning done." + +# ------------------------------------------------------------------ +log "--- Step 3/7: Formatting ---" +if [ "$UEFI" = "1" ]; then + mkfs.vfat -F32 -n EFI "$PART_EFI" + log " $PART_EFI formatted as vfat (EFI)" +fi +mkfs.ext4 -F -L bee-root "$PART_ROOT" +log " $PART_ROOT formatted as ext4" + +# ------------------------------------------------------------------ +log "--- Step 4/7: Mounting target ---" +mkdir -p "$MOUNT_ROOT" +mount "$PART_ROOT" "$MOUNT_ROOT" +if [ "$UEFI" = "1" ]; then + mkdir -p "${MOUNT_ROOT}/boot/efi" + mount "$PART_EFI" "${MOUNT_ROOT}/boot/efi" +fi +log " Mounted." + +# ------------------------------------------------------------------ +log "--- Step 5/7: Unpacking filesystem (this takes 10-20 minutes) ---" +log " Source: $SQUASHFS" +log " Target: $MOUNT_ROOT" +unsquashfs -f -d "$MOUNT_ROOT" "$SQUASHFS" 2>&1 | \ + grep -E '^\[|^inod|^created|^extract' | \ + while read -r line; do log " $line"; done || true +log " Unpack complete." + +# ------------------------------------------------------------------ +log "--- Step 6/7: Configuring installed system ---" + +# Write /etc/fstab +ROOT_UUID=$(blkid -s UUID -o value "$PART_ROOT") +log " Root UUID: $ROOT_UUID" +cat > "${MOUNT_ROOT}/etc/fstab" <> "${MOUNT_ROOT}/etc/fstab" +fi +log " fstab written." + +# Remove live-boot persistence markers so installed system boots normally +rm -f "${MOUNT_ROOT}/etc/live/boot.conf" 2>/dev/null || true +rm -f "${MOUNT_ROOT}/etc/live/live.conf" 2>/dev/null || true + +# Bind mount virtual filesystems for chroot +mount --bind /dev "${MOUNT_ROOT}/dev" +mount --bind /proc "${MOUNT_ROOT}/proc" +mount --bind /sys "${MOUNT_ROOT}/sys" +[ "$UEFI" = "1" ] && mount --bind /sys/firmware/efi/efivars "${MOUNT_ROOT}/sys/firmware/efi/efivars" 2>/dev/null || true + +# ------------------------------------------------------------------ +log "--- Step 7/7: Installing GRUB bootloader ---" +if [ "$UEFI" = "1" ]; then + chroot "$MOUNT_ROOT" grub-install \ + --target=x86_64-efi \ + --efi-directory=/boot/efi \ + --bootloader-id=bee \ + --recheck 2>&1 | while read -r line; do log " $line"; done || true +else + chroot "$MOUNT_ROOT" grub-install \ + --target=i386-pc \ + --recheck \ + "$DEVICE" 2>&1 | while read -r line; do log " $line"; done || true +fi +chroot "$MOUNT_ROOT" update-grub 2>&1 | while read -r line; do log " $line"; done || true +log " GRUB installed." + +# ------------------------------------------------------------------ +# Cleanup +log "--- Cleanup ---" +umount "${MOUNT_ROOT}/sys/firmware/efi/efivars" 2>/dev/null || true +umount "${MOUNT_ROOT}/sys" 2>/dev/null || true +umount "${MOUNT_ROOT}/proc" 2>/dev/null || true +umount "${MOUNT_ROOT}/dev" 2>/dev/null || true +[ "$UEFI" = "1" ] && umount "${MOUNT_ROOT}/boot/efi" 2>/dev/null || true +umount "$MOUNT_ROOT" 2>/dev/null || true +rmdir "$MOUNT_ROOT" 2>/dev/null || true + +log "" +log "=== INSTALLATION COMPLETE ===" +log "Remove the ISO and reboot to start the installed system."