Files
miasma-installer/tui/steps/install.go
tumillanino f05bd8b929
Some checks failed
Build / build (push) Failing after 4m53s
added functionality to delete existing partitions before reinstall
2025-11-12 15:55:15 +11:00

834 lines
26 KiB
Go

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 {
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) 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()
}