Files
miasma-installer/tui/steps/install.go
tumillanino ee79d720ec
Some checks failed
Build / build (push) Failing after 4m56s
updated styling and installation steps
2025-10-31 22:59:56 +11:00

471 lines
12 KiB
Go

package steps
import (
"bufio"
"fmt"
"miasma-installer/config"
"miasma-installer/tui/styles"
"os"
"os/exec"
"path/filepath"
"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 {
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) partitionDisk() error {
disk := m.config.Disk
wipeDisk := exec.Command("wipefs", "-af", disk)
if err := wipeDisk.Run(); err != nil {
return err
}
sgdisk := exec.Command("sgdisk", "-Z", disk)
if err := sgdisk.Run(); err != nil {
return err
}
createEFI := exec.Command("sgdisk", "-n", "1:0:+512M", "-t", "1:ef00", disk)
if err := createEFI.Run(); err != nil {
return err
}
createRoot := exec.Command("sgdisk", "-n", "2:0:0", "-t", "2:8300", disk)
if err := createRoot.Run(); err != nil {
return err
}
time.Sleep(1 * time.Second)
exec.Command("partprobe", disk).Run()
return nil
}
func (m InstallModel) formatPartitions() error {
disk := m.config.Disk
efiPart := disk + "1"
rootPart := disk + "2"
if strings.Contains(disk, "nvme") || strings.Contains(disk, "mmcblk") {
efiPart = disk + "p1"
rootPart = disk + "p2"
}
mkfsEFI := exec.Command("mkfs.fat", "-F32", efiPart)
if err := mkfsEFI.Run(); err != nil {
return err
}
if m.config.EnableLUKS {
cryptsetup := exec.Command("cryptsetup", "luksFormat", "--type", "luks2", rootPart)
cryptsetup.Stdin = strings.NewReader(m.config.RootPassword + "\n")
if err := cryptsetup.Run(); err != nil {
return err
}
cryptOpen := exec.Command("cryptsetup", "open", rootPart, "cryptroot")
cryptOpen.Stdin = strings.NewReader(m.config.RootPassword + "\n")
if err := cryptOpen.Run(); err != nil {
return err
}
rootPart = "/dev/mapper/cryptroot"
}
mkfsBtrfs := exec.Command("mkfs.btrfs", "-f", rootPart)
if err := mkfsBtrfs.Run(); err != nil {
return err
}
return nil
}
func (m InstallModel) mountFilesystems() error {
disk := m.config.Disk
rootPart := disk + "2"
efiPart := disk + "1"
if strings.Contains(disk, "nvme") || strings.Contains(disk, "mmcblk") {
efiPart = disk + "p1"
rootPart = disk + "p2"
}
if m.config.EnableLUKS {
rootPart = "/dev/mapper/cryptroot"
}
os.MkdirAll("/mnt", 0755)
mount := exec.Command("mount", rootPart, "/mnt")
if err := mount.Run(); err != nil {
return err
}
btrfsOpts := "subvol=@,compress=zstd,noatime"
for _, subvol := range []string{"@", "@home", "@var", "@snapshots"} {
create := exec.Command("btrfs", "subvolume", "create", "/mnt/"+subvol)
create.Run()
}
exec.Command("umount", "/mnt").Run()
mount = exec.Command("mount", "-o", btrfsOpts, rootPart, "/mnt")
if err := mount.Run(); err != nil {
return err
}
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")
mountHome.Run()
mountVar := exec.Command("mount", "-o", "subvol=@var,compress=zstd,noatime", rootPart, "/mnt/var")
mountVar.Run()
mountSnap := exec.Command("mount", "-o", "subvol=@snapshots,compress=zstd,noatime", rootPart, "/mnt/.snapshots")
mountSnap.Run()
mountEFI := exec.Command("mount", efiPart, "/mnt/boot")
if err := mountEFI.Run(); err != nil {
return err
}
return nil
}
func (m InstallModel) installBase() error {
packages := []string{
"base",
"base-devel",
"linux-hardened",
"linux-firmware",
"btrfs-progs",
"neovim",
"tmux",
"git",
"networkmanager",
"sudo",
}
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 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)
chroot([]string{"locale-gen"})
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"})
useraddCmd := fmt.Sprintf("useradd -m -G wheel -s /bin/bash %s", m.config.Username)
chroot([]string{"sh", "-c", useraddCmd})
passwdCmd := fmt.Sprintf("echo '%s:%s' | chpasswd", m.config.Username, m.config.UserPassword)
chroot([]string{"sh", "-c", passwdCmd})
rootPasswdCmd := fmt.Sprintf("echo 'root:%s' | chpasswd", m.config.RootPassword)
chroot([]string{"sh", "-c", rootPasswdCmd})
sudoers := "%wheel ALL=(ALL:ALL) ALL\n"
os.WriteFile("/mnt/etc/sudoers.d/wheel", []byte(sudoers), 0440)
chroot([]string{"systemctl", "enable", "NetworkManager"})
return nil
}
func (m InstallModel) installBootloader() error {
chroot([]string{"bootctl", "install"})
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"
if strings.Contains(disk, "nvme") || strings.Contains(disk, "mmcblk") {
rootPart = disk + "p2"
}
rootUUID := ""
if m.config.EnableLUKS {
blkid := exec.Command("blkid", "-s", "UUID", "-o", "value", rootPart)
output, _ := blkid.Output()
rootUUID = strings.TrimSpace(string(output))
} else {
rootPart = "/dev/mapper/cryptroot"
blkid := exec.Command("blkid", "-s", "UUID", "-o", "value", rootPart)
output, _ := blkid.Output()
rootUUID = strings.TrimSpace(string(output))
}
options := fmt.Sprintf("root=UUID=%s rootflags=subvol=@ rw", rootUUID)
if m.config.EnableLUKS {
cryptUUID := ""
blkid := exec.Command("blkid", "-s", "UUID", "-o", "value", disk+"2")
output, _ := blkid.Output()
cryptUUID = strings.TrimSpace(string(output))
options = fmt.Sprintf("cryptdevice=UUID=%s:cryptroot root=/dev/mapper/cryptroot rootflags=subvol=@ rw", cryptUUID)
}
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 {
mkinitcpio, _ := os.ReadFile("/mnt/etc/mkinitcpio.conf")
newConf := strings.Replace(string(mkinitcpio),
"HOOKS=(base udev autodetect modconf block filesystems keyboard fsck)",
"HOOKS=(base udev autodetect keyboard keymap consolefont modconf block encrypt filesystems fsck)",
1)
os.WriteFile("/mnt/etc/mkinitcpio.conf", []byte(newConf), 0644)
chroot([]string{"mkinitcpio", "-P"})
}
return nil
}
func (m InstallModel) installDesktop() error {
chroot([]string{"pacman", "-Sy", "--noconfirm", "cosmic-session", "cosmic-greeter"})
chroot([]string{"systemctl", "enable", "cosmic-greeter"})
return nil
}
func (m InstallModel) runPostInstallScripts() error {
scriptsDir := "./scripts"
entries, err := os.ReadDir(scriptsDir)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
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
}
chrootScript := "/tmp/" + entry.Name()
os.WriteFile("/mnt"+chrootScript, scriptContent, 0755)
if output, err := chroot([]string{chrootScript}); err != nil {
return fmt.Errorf("script %s failed: %w\nOutput: %s", entry.Name(), err, output)
}
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, nil
}
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\nYou can now reboot your system."
content.WriteString(styles.BoxStyle.Render(success))
content.WriteString("\n\n")
content.WriteString(styles.RenderHelp("Ctrl+C", "Exit"))
}
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()
}