package steps import ( "bufio" "fmt" "miasma-installer/config" "miasma-installer/tui/styles" "os" "os/exec" "path/filepath" "sort" "strings" "time" tea "github.com/charmbracelet/bubbletea" ) type InstallModel struct { config *config.InstallConfig Finished bool status string progress []string err error currentStep int totalSteps int } func NewInstallModel(disk string, enableLUKS bool, hostname string, username string, userPassword string, rootPassword string) InstallModel { return InstallModel{ config: &config.InstallConfig{ Disk: disk, EnableLUKS: enableLUKS, Hostname: hostname, Username: username, UserPassword: userPassword, RootPassword: rootPassword, Timezone: "UTC", Locale: "en_US.UTF-8", }, status: "Preparing installation...", progress: []string{}, currentStep: 0, totalSteps: 8, } } func (m InstallModel) Init() tea.Cmd { return m.startInstall() } type installCompleteMsg struct { err error } type installProgressMsg struct { step int message string } func (m InstallModel) startInstall() tea.Cmd { return func() tea.Msg { // Save archinstall configuration files if err := m.saveConfigurationFiles(); err != nil { return installCompleteMsg{err: fmt.Errorf("failed to save configuration files: %w", err)} } if err := m.performInstall(); err != nil { return installCompleteMsg{err: err} } return installCompleteMsg{err: nil} } } func (m InstallModel) performInstall() error { // Check for required tools before starting installation if err := m.checkRequiredTools(); err != nil { return fmt.Errorf("missing required tools: %w", err) } m.updateProgress(1, "Partitioning disk...") if err := m.partitionDisk(); err != nil { return fmt.Errorf("partitioning failed: %w", err) } m.updateProgress(2, "Formatting partitions...") if err := m.formatPartitions(); err != nil { return fmt.Errorf("formatting failed: %w", err) } m.updateProgress(3, "Mounting filesystems...") if err := m.mountFilesystems(); err != nil { return fmt.Errorf("mounting failed: %w", err) } m.updateProgress(4, "Installing base system...") if err := m.installBase(); err != nil { return fmt.Errorf("base install failed: %w", err) } m.updateProgress(5, "Configuring system...") if err := m.configureSystem(); err != nil { return fmt.Errorf("configuration failed: %w", err) } m.updateProgress(6, "Installing bootloader...") if err := m.installBootloader(); err != nil { return fmt.Errorf("bootloader install failed: %w", err) } m.updateProgress(7, "Installing desktop and packages...") if err := m.installDesktop(); err != nil { return fmt.Errorf("desktop install failed: %w", err) } m.updateProgress(8, "Running post-install scripts...") if err := m.runPostInstallScripts(); err != nil { return fmt.Errorf("post-install scripts failed: %w", err) } return nil } func (m InstallModel) checkRequiredTools() error { requiredTools := []string{ "sgdisk", "wipefs", "partprobe", "mkfs.fat", "mkfs.btrfs", "mount", "umount", "dd", "blkid", "fuser", } missingTools := []string{} for _, tool := range requiredTools { if _, err := exec.LookPath(tool); err != nil { missingTools = append(missingTools, tool) } } if len(missingTools) > 0 { // Try to install missing tools if err := m.installMissingTools(missingTools); err != nil { return fmt.Errorf("failed to install missing tools %v: %w", missingTools, err) } } return nil } func (m InstallModel) installMissingTools(tools []string) error { // Install required packages for partitioning tools requiredPackages := []string{} // Map tools to packages that provide them toolToPackage := map[string]string{ "sgdisk": "gptfdisk", "wipefs": "util-linux", "partprobe": "parted", "mkfs.fat": "dosfstools", "mkfs.btrfs": "btrfs-progs", "blkid": "util-linux", "fuser": "psmisc", } // Determine which packages need to be installed neededPackages := make(map[string]bool) for _, tool := range tools { if pkg, exists := toolToPackage[tool]; exists { neededPackages[pkg] = true } } // Convert map to slice for pkg := range neededPackages { requiredPackages = append(requiredPackages, pkg) } if len(requiredPackages) == 0 { return nil } // Install the packages m.updateProgress(m.currentStep, "Installing required tools...") cmd := exec.Command("pacman", "-Sy", "--noconfirm") cmd.Args = append(cmd.Args, requiredPackages...) if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to install required packages: %w\nOutput: %s", err, string(output)) } return nil } func (m InstallModel) updateProgress(step int, message string) { m.currentStep = step m.status = message time.Sleep(100 * time.Millisecond) } func (m InstallModel) saveConfigurationFiles() error { m.updateProgress(0, "Saving configuration files...") // Save user configuration if err := m.config.SaveArchInstallConfig("/tmp/user_configuration.json"); err != nil { return fmt.Errorf("failed to save user configuration: %w", err) } // Save user credentials if err := m.config.SaveUserCredentials("/tmp/user_credentials.json"); err != nil { return fmt.Errorf("failed to save user credentials: %w", err) } // Copy configuration files to target system os.MkdirAll("/mnt/var/log/archinstall", 0755) exec.Command("cp", "/tmp/user_configuration.json", "/mnt/var/log/archinstall/").Run() exec.Command("cp", "/tmp/user_credentials.json", "/mnt/var/log/archinstall/").Run() return nil } func (m InstallModel) partitionDisk() error { disk := m.config.Disk m.updateProgress(m.currentStep, "Clearing existing partition table...") // Ensure the disk device exists if _, err := os.Stat(disk); os.IsNotExist(err) { return fmt.Errorf("disk device %s does not exist", disk) } // Kill all processes using the disk killCmd := exec.Command("fuser", "-k", disk) killCmd.Run() // Ignore errors as the device might not be in use // Wait a moment for processes to terminate time.Sleep(500 * time.Millisecond) // Wipe filesystem signatures wipeDisk := exec.Command("wipefs", "-af", disk) if output, err := wipeDisk.CombinedOutput(); err != nil { return fmt.Errorf("failed to wipe filesystem signatures: %w\nOutput: %s", err, string(output)) } // Zap (clear) the GPT and MBR partition table sgdiskZap := exec.Command("sgdisk", "--zap-all", disk) if output, err := sgdiskZap.CombinedOutput(); err != nil { return fmt.Errorf("failed to zap partition table: %w\nOutput: %s", err, string(output)) } // Clear with dd for additional safety ddClear := exec.Command("dd", "if=/dev/zero", "of="+disk, "bs=1M", "count=100", "status=none") if output, err := ddClear.CombinedOutput(); err != nil { return fmt.Errorf("failed to clear disk header: %w\nOutput: %s", err, string(output)) } // Ensure the kernel re-reads the partition table partprobe := exec.Command("partprobe", disk) partprobe.Run() // Wait for the system to recognize the changes time.Sleep(1 * time.Second) m.updateProgress(m.currentStep, "Creating new partition table...") // Create new GPT partition table sgdiskNew := exec.Command("sgdisk", "--clear", disk) if output, err := sgdiskNew.CombinedOutput(); err != nil { return fmt.Errorf("failed to create new partition table: %w\nOutput: %s", err, string(output)) } // Create EFI System Partition (512MB, FAT32) createEFI := exec.Command("sgdisk", "--new=1:0:+512M", "--typecode=1:EF00", "--change-name=1:EFI System Partition", disk) if output, err := createEFI.CombinedOutput(); err != nil { return fmt.Errorf("failed to create EFI partition: %w\nOutput: %s", err, string(output)) } // Create root partition (remaining space, Linux filesystem) createRoot := exec.Command("sgdisk", "--new=2:0:0", "--typecode=2:8300", "--change-name=2:Linux Root", disk) if output, err := createRoot.CombinedOutput(); err != nil { return fmt.Errorf("failed to create root partition: %w\nOutput: %s", err, string(output)) } // Notify the kernel of partition table changes partprobe2 := exec.Command("partprobe", disk) partprobe2.Run() // Wait for the system to recognize the new partitions time.Sleep(1 * time.Second) return nil } func (m InstallModel) formatPartitions() error { disk := m.config.Disk efiPart := disk + "1" rootPart := disk + "2" // Wait a bit more to ensure partitions are recognized time.Sleep(2 * time.Second) // Re-probe the partition table to ensure kernel recognizes the new partitions exec.Command("partprobe", disk).Run() time.Sleep(500 * time.Millisecond) // Verify partitions exist if _, err := os.Stat(efiPart); os.IsNotExist(err) { if strings.Contains(disk, "nvme") || strings.Contains(disk, "mmcblk") { efiPart = disk + "p1" rootPart = disk + "p2" } } // Double-check that the partitions exist with the correct naming if _, err := os.Stat(efiPart); os.IsNotExist(err) { // Try to list available partitions to help with debugging lsblk := exec.Command("lsblk", "-f", disk) if output, err := lsblk.CombinedOutput(); err == nil { return fmt.Errorf("EFI partition %s not found after partitioning. Available partitions:\n%s", efiPart, string(output)) } return fmt.Errorf("EFI partition %s not found after partitioning", efiPart) } mkfsEFI := exec.Command("mkfs.fat", "-F32", efiPart) if output, err := mkfsEFI.CombinedOutput(); err != nil { return fmt.Errorf("failed to format EFI partition: %w\nOutput: %s", err, string(output)) } if m.config.EnableLUKS { cryptsetup := exec.Command("cryptsetup", "luksFormat", "--type", "luks2", rootPart) cryptsetup.Stdin = strings.NewReader(m.config.RootPassword + "\n") if output, err := cryptsetup.CombinedOutput(); err != nil { return fmt.Errorf("failed to format LUKS partition: %w\nOutput: %s", err, string(output)) } cryptOpen := exec.Command("cryptsetup", "open", rootPart, "cryptroot") cryptOpen.Stdin = strings.NewReader(m.config.RootPassword + "\n") if output, err := cryptOpen.CombinedOutput(); err != nil { return fmt.Errorf("failed to open LUKS partition: %w\nOutput: %s", err, string(output)) } rootPart = "/dev/mapper/cryptroot" } mkfsBtrfs := exec.Command("mkfs.btrfs", "-f", rootPart) if output, err := mkfsBtrfs.CombinedOutput(); err != nil { return fmt.Errorf("failed to format root partition: %w\nOutput: %s", err, string(output)) } return nil } func (m InstallModel) mountFilesystems() error { disk := m.config.Disk rootPart := disk + "2" efiPart := disk + "1" // Wait for partitions to be fully recognized time.Sleep(1 * time.Second) exec.Command("partprobe", disk).Run() time.Sleep(500 * time.Millisecond) // Handle NVMe and mmcblk device naming if strings.Contains(disk, "nvme") || strings.Contains(disk, "mmcblk") { efiPart = disk + "p1" rootPart = disk + "p2" } // Verify partitions exist before mounting if _, err := os.Stat(efiPart); os.IsNotExist(err) { return fmt.Errorf("EFI partition %s not found", efiPart) } if _, err := os.Stat(rootPart); os.IsNotExist(err) { return fmt.Errorf("root partition %s not found", rootPart) } if m.config.EnableLUKS { // Check if cryptroot is already open if _, err := os.Stat("/dev/mapper/cryptroot"); os.IsNotExist(err) { cryptOpen := exec.Command("cryptsetup", "open", rootPart, "cryptroot") cryptOpen.Stdin = strings.NewReader(m.config.RootPassword + "\n") if output, err := cryptOpen.CombinedOutput(); err != nil { return fmt.Errorf("failed to open LUKS partition: %w\nOutput: %s", err, string(output)) } } rootPart = "/dev/mapper/cryptroot" } os.MkdirAll("/mnt", 0755) mount := exec.Command("mount", rootPart, "/mnt") if output, err := mount.CombinedOutput(); err != nil { return fmt.Errorf("failed to mount root partition: %w\nOutput: %s", err, string(output)) } // Create btrfs subvolumes btrfsOpts := "subvol=@,compress=zstd,noatime" for _, subvol := range []string{"@", "@home", "@var", "@snapshots"} { create := exec.Command("btrfs", "subvolume", "create", "/mnt/"+subvol) if output, err := create.CombinedOutput(); err != nil { return fmt.Errorf("failed to create subvolume %s: %w\nOutput: %s", subvol, err, string(output)) } } exec.Command("umount", "/mnt").Run() // Mount root with subvolume mount = exec.Command("mount", "-o", btrfsOpts, rootPart, "/mnt") if output, err := mount.CombinedOutput(); err != nil { return fmt.Errorf("failed to mount root with subvolumes: %w\nOutput: %s", err, string(output)) } os.MkdirAll("/mnt/home", 0755) os.MkdirAll("/mnt/var", 0755) os.MkdirAll("/mnt/.snapshots", 0755) os.MkdirAll("/mnt/boot", 0755) mountHome := exec.Command("mount", "-o", "subvol=@home,compress=zstd,noatime", rootPart, "/mnt/home") if output, err := mountHome.CombinedOutput(); err != nil { return fmt.Errorf("failed to mount home subvolume: %w\nOutput: %s", err, string(output)) } mountVar := exec.Command("mount", "-o", "subvol=@var,compress=zstd,noatime", rootPart, "/mnt/var") if output, err := mountVar.CombinedOutput(); err != nil { return fmt.Errorf("failed to mount var subvolume: %w\nOutput: %s", err, string(output)) } mountSnap := exec.Command("mount", "-o", "subvol=@snapshots,compress=zstd,noatime", rootPart, "/mnt/.snapshots") if output, err := mountSnap.CombinedOutput(); err != nil { return fmt.Errorf("failed to mount snapshots subvolume: %w\nOutput: %s", err, string(output)) } mountEFI := exec.Command("mount", efiPart, "/mnt/boot") if output, err := mountEFI.CombinedOutput(); err != nil { return fmt.Errorf("failed to mount EFI partition: %w\nOutput: %s", err, string(output)) } return nil } func (m InstallModel) installBase() error { // Base Miasma OS packages with security hardening packages := []string{ "base", "base-devel", "linux-hardened", "linux-firmware", "btrfs-progs", "neovim", // Default text editor as specified in product description "zsh", // Default shell as specified in product description "alacritty", // Default terminal as specified in product description "tmux", "git", "networkmanager", "opendoas", // Replace sudo with doas as specified in product description "firejail", "apparmor", "nftables", // Using nftables instead of ufw as requested "hardened-malloc", // Hardened malloc by GrapheneOS } pacstrap := exec.Command("pacstrap", append([]string{"/mnt"}, packages...)...) pacstrap.Stdout = os.Stdout pacstrap.Stderr = os.Stderr if err := pacstrap.Run(); err != nil { return err } return nil } func (m InstallModel) configureSystem() error { genfstab := exec.Command("genfstab", "-U", "/mnt") fstab, err := genfstab.Output() if err != nil { return fmt.Errorf("genfstab failed: %w", err) } os.WriteFile("/mnt/etc/fstab", fstab, 0644) os.WriteFile("/mnt/etc/hostname", []byte(m.config.Hostname+"\n"), 0644) hosts := fmt.Sprintf("127.0.0.1\tlocalhost\n::1\t\tlocalhost\n127.0.1.1\t%s.localdomain\t%s\n", m.config.Hostname, m.config.Hostname) os.WriteFile("/mnt/etc/hosts", []byte(hosts), 0644) os.WriteFile("/mnt/etc/locale.gen", []byte("en_US.UTF-8 UTF-8\n"), 0644) if _, err := chroot([]string{"locale-gen"}); err != nil { return fmt.Errorf("locale-gen failed: %w", err) } os.WriteFile("/mnt/etc/locale.conf", []byte("LANG=en_US.UTF-8\n"), 0644) chroot([]string{"ln", "-sf", "/usr/share/zoneinfo/UTC", "/etc/localtime"}) chroot([]string{"hwclock", "--systohc"}) // Create user with zsh as default shell useraddCmd := fmt.Sprintf("useradd -m -G wheel -s /bin/zsh %s", m.config.Username) if _, err := chroot([]string{"sh", "-c", useraddCmd}); err != nil { return fmt.Errorf("useradd failed: %w", err) } passwdCmd := fmt.Sprintf("echo '%s:%s' | chpasswd", m.config.Username, m.config.UserPassword) if _, err := chroot([]string{"sh", "-c", passwdCmd}); err != nil { return fmt.Errorf("user password set failed: %w", err) } // Set root password rootPasswd := m.config.RootPassword if rootPasswd == "" { rootPasswd = m.config.UserPassword } rootPasswdCmd := fmt.Sprintf("echo 'root:%s' | chpasswd", rootPasswd) if _, err := chroot([]string{"sh", "-c", rootPasswdCmd}); err != nil { return fmt.Errorf("root password set failed: %w", err) } // Configure doas instead of sudo doasConf := fmt.Sprintf("permit persist :wheel\npermit nopass root\n") os.WriteFile("/mnt/etc/doas.conf", []byte(doasConf), 0600) // Configure sudoers for compatibility (limited use) sudoers := "%wheel ALL=(ALL:ALL) ALL\n" os.WriteFile("/mnt/etc/sudoers.d/wheel", []byte(sudoers), 0440) // Enable required services services := []string{ "NetworkManager", "ufw", "apparmor", } for _, service := range services { if _, err := chroot([]string{"systemctl", "enable", service}); err != nil { return fmt.Errorf("failed to enable %s: %w", service, err) } } // Apply system hardening if err := m.applySystemHardening(); err != nil { return fmt.Errorf("system hardening failed: %w", err) } // Configure default shell and applications if err := m.configureDefaults(); err != nil { return fmt.Errorf("default configuration failed: %w", err) } return nil } func (m InstallModel) applySystemHardening() error { // Configure kernel parameters for hardening sysctlConf := `# Miasma OS Security Hardening kernel.dmesg_restrict = 1 kernel.kptr_restrict = 2 kernel.unprivileged_bpf_disabled = 1 net.core.bpf_jit_harden = 2 kernel.yama.ptrace_scope = 2 net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv6.conf.all.accept_redirects = 0 net.ipv6.conf.default.accept_redirects = 0 net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.default.send_redirects = 0 net.ipv4.icmp_echo_ignore_all = 1 ` if err := os.WriteFile("/mnt/etc/sysctl.d/99-hardening.conf", []byte(sysctlConf), 0644); err != nil { return fmt.Errorf("failed to write sysctl config: %w", err) } // Configure nftables (replacing ufw) if _, err := chroot([]string{"pacman", "-Sy", "--noconfirm", "nftables"}); err != nil { return fmt.Errorf("failed to install nftables: %w", err) } // Set secure umask profileAppend := "\n# Secure umask\numask 077\n" f, err := os.OpenFile("/mnt/etc/profile", os.O_APPEND|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("failed to open profile: %w", err) } defer f.Close() if _, err := f.WriteString(profileAppend); err != nil { return fmt.Errorf("failed to append to profile: %w", err) } // Disable core dumps limitsAppend := "\n# Disable core dumps\n* hard core 0\n" f2, err := os.OpenFile("/mnt/etc/security/limits.conf", os.O_APPEND|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("failed to open limits.conf: %w", err) } defer f2.Close() if _, err := f2.WriteString(limitsAppend); err != nil { return fmt.Errorf("failed to append to limits.conf: %w", err) } return nil } func (m InstallModel) configureDefaults() error { // Install and configure Ungoogled Chromium with Wayland support chromiumPackages := []string{ "ungoogled-chromium", "chromium-web-store", // For extension installation } for _, pkg := range chromiumPackages { if _, err := chroot([]string{"pacman", "-Sy", "--noconfirm", pkg}); err != nil { return fmt.Errorf("failed to install %s: %w", pkg, err) } } // Configure default applications for the user userHome := fmt.Sprintf("/home/%s", m.config.Username) chromiumDir := fmt.Sprintf("/mnt%s/.config/chromium", userHome) os.MkdirAll(chromiumDir, 0755) if err := os.WriteFile(fmt.Sprintf("%s/Local State", chromiumDir), []byte("{}"), 0644); err != nil { return fmt.Errorf("failed to create chromium config: %w", err) } // Set up LazyVim configuration for Neovim lazyVimConfig := `return { colorscheme = "kanagawa-dragon", defaults = { indent = 2, tabstop = 2, shiftwidth = 2, }, } ` lazyDir := fmt.Sprintf("/mnt%s/.config/nvim", userHome) os.MkdirAll(lazyDir, 0755) if err := os.WriteFile(fmt.Sprintf("%s/lazyvim.json", lazyDir), []byte(lazyVimConfig), 0644); err != nil { return fmt.Errorf("failed to create lazyvim config: %w", err) } // Set up Zsh configuration with Kanagawa theme zshrc := fmt.Sprintf(`# Miasma OS Zsh Configuration export ZSH="/home/%s/.oh-my-zsh" ZSH_THEME="kanagawa" plugins=(git sudo extract) source $ZSH/oh-my-zsh.sh # Aliases alias ls="ls --color=auto" alias la="ls -la" alias vi="nvim" alias vim="nvim" # Default applications export EDITOR="nvim" export TERMINAL="alacritty" `, m.config.Username) zshrcPath := fmt.Sprintf("/mnt%s/.zshrc", userHome) if err := os.WriteFile(zshrcPath, []byte(zshrc), 0644); err != nil { return fmt.Errorf("failed to create zshrc: %w", err) } // Set proper ownership for user files chownCmd := fmt.Sprintf("chown -R %s:%s /home/%s", m.config.Username, m.config.Username, m.config.Username) if _, err := chroot([]string{"sh", "-c", chownCmd}); err != nil { return fmt.Errorf("failed to set user ownership: %w", err) } return nil } func (m InstallModel) installBootloader() error { if _, err := chroot([]string{"bootctl", "install"}); err != nil { return fmt.Errorf("bootctl install failed: %w", err) } loaderConf := `default arch.conf timeout 3 console-mode max editor no ` os.WriteFile("/mnt/boot/loader/loader.conf", []byte(loaderConf), 0644) disk := m.config.Disk rootPart := disk + "2" // Handle NVMe and mmcblk device naming if strings.Contains(disk, "nvme") || strings.Contains(disk, "mmcblk") { rootPart = disk + "p2" } // For LUKS, we need to reference the encrypted device, not the decrypted one // The bootloader needs to unlock the device first var options string if m.config.EnableLUKS { // Get UUID of the encrypted partition (not the mapped device) blkid := exec.Command("blkid", "-s", "UUID", "-o", "value", disk+"2") if strings.Contains(disk, "nvme") || strings.Contains(disk, "mmcblk") { blkid = exec.Command("blkid", "-s", "UUID", "-o", "value", disk+"p2") } output, err := blkid.Output() if err != nil { return fmt.Errorf("failed to get encrypted partition UUID: %w", err) } cryptUUID := strings.TrimSpace(string(output)) options = fmt.Sprintf("cryptdevice=UUID=%s:cryptroot root=/dev/mapper/cryptroot rootflags=subvol=@ rw quiet splash", cryptUUID) } else { // Get UUID of the root partition blkid := exec.Command("blkid", "-s", "UUID", "-o", "value", rootPart) output, err := blkid.Output() if err != nil { return fmt.Errorf("failed to get root partition UUID: %w", err) } rootUUID := strings.TrimSpace(string(output)) options = fmt.Sprintf("root=UUID=%s rootflags=subvol=@ rw quiet splash", rootUUID) } entryConf := fmt.Sprintf(`title Miasma OS linux /vmlinuz-linux-hardened initrd /initramfs-linux-hardened.img options %s `, options) os.WriteFile("/mnt/boot/loader/entries/arch.conf", []byte(entryConf), 0644) if m.config.EnableLUKS { mkinitcpioConf, err := os.ReadFile("/mnt/etc/mkinitcpio.conf") if err != nil { return fmt.Errorf("failed to read mkinitcpio.conf: %w", err) } newConf := string(mkinitcpioConf) newConf = strings.Replace(newConf, "HOOKS=(base udev autodetect modconf kms keyboard keymap consolefont block filesystems fsck)", "HOOKS=(base udev autodetect keyboard keymap consolefont modconf kms block encrypt filesystems fsck)", 1) newConf = strings.Replace(newConf, "HOOKS=(base udev autodetect modconf block filesystems keyboard fsck)", "HOOKS=(base udev autodetect keyboard keymap consolefont modconf block encrypt filesystems fsck)", 1) if err := os.WriteFile("/mnt/etc/mkinitcpio.conf", []byte(newConf), 0644); err != nil { return fmt.Errorf("failed to write mkinitcpio.conf: %w", err) } if _, err := chroot([]string{"mkinitcpio", "-P"}); err != nil { return fmt.Errorf("mkinitcpio failed: %w", err) } } return nil } func (m InstallModel) installDesktop() error { // Install Cosmic Desktop as the only supported desktop environment cosmicPackages := []string{ "cosmic-session", "cosmic-greeter", "cosmic-files", "cosmic-edit", "cosmic-term", "cosmic-store", "cosmic-settings", "xorg-xwayland", "xwayland-satellite", // XWayland-Satellite for better X11 app isolation } for _, pkg := range cosmicPackages { if _, err := chroot([]string{"pacman", "-Sy", "--noconfirm", pkg}); err != nil { return fmt.Errorf("failed to install %s: %w", pkg, err) } } // Enable Cosmic services services := []string{ "cosmic-greeter", "NetworkManager", } for _, service := range services { if _, err := chroot([]string{"systemctl", "enable", service}); err != nil { return fmt.Errorf("failed to enable %s: %w", service, err) } } return nil } func (m InstallModel) runPostInstallScripts() error { // Copy overlay files if they exist overlayDir := "./overlay" if _, err := os.Stat(overlayDir); err == nil { fmt.Println("Applying system overlays...") cp := exec.Command("cp", "-r", overlayDir+"/.", "/mnt/") if output, err := cp.CombinedOutput(); err != nil { return fmt.Errorf("overlay copy failed: %w\nOutput: %s", err, output) } } // Run post-install scripts scriptsDir := "./scripts" entries, err := os.ReadDir(scriptsDir) if err != nil { if os.IsNotExist(err) { return nil } return err } // Sort scripts to ensure they run in order sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sh") { continue } scriptPath := filepath.Join(scriptsDir, entry.Name()) scriptContent, err := os.ReadFile(scriptPath) if err != nil { return err } // Copy script to chroot environment chrootScript := "/tmp/" + entry.Name() os.WriteFile("/mnt"+chrootScript, scriptContent, 0755) // Run script in chroot environment if output, err := chroot([]string{"sh", chrootScript}); err != nil { return fmt.Errorf("script %s failed: %w\nOutput: %s", entry.Name(), err, output) } // Clean up script os.Remove("/mnt" + chrootScript) } return nil } func chroot(args []string) (string, error) { cmd := exec.Command("arch-chroot", append([]string{"/mnt"}, args...)...) output, err := cmd.CombinedOutput() return string(output), err } func (m InstallModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case installCompleteMsg: if msg.err != nil { m.err = msg.err m.status = "Installation failed" } else { m.status = "Installation complete!" } m.Finished = true return m, tea.Quit } return m, nil } func (m InstallModel) View() string { var content strings.Builder content.WriteString(styles.TitleStyle.Render("⚙️ Installing Miasma OS")) content.WriteString("\n\n") progress := fmt.Sprintf("Step %d/%d", m.currentStep, m.totalSteps) content.WriteString(styles.ProgressTextStyle.Render(progress)) content.WriteString("\n") bar := strings.Repeat("█", m.currentStep) + strings.Repeat("░", m.totalSteps-m.currentStep) content.WriteString(styles.ProgressBarStyle.Render(bar)) content.WriteString("\n\n") content.WriteString(styles.SpinnerStyle.Render("● ") + m.status) content.WriteString("\n\n") if m.err != nil { errMsg := fmt.Sprintf("✗ Error: %v", m.err) content.WriteString(styles.ErrorBoxStyle.Render(errMsg)) content.WriteString("\n\n") content.WriteString(styles.HelpStyle.Render("Press Ctrl+C to exit")) } if m.Finished && m.err == nil { success := "✓ Installation successful!\n\nPress Ctrl+C to exit and reboot." content.WriteString(styles.BoxStyle.Render(success)) content.WriteString("\n\n") content.WriteString(styles.RenderHelp("Ctrl+C", "Exit and reboot")) } return styles.AppStyle.Render(content.String()) } func readLines(path string) ([]string, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() var lines []string scanner := bufio.NewScanner(file) for scanner.Scan() { lines = append(lines, scanner.Text()) } return lines, scanner.Err() }