This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package steps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bufio"
|
||||
"fmt"
|
||||
"miasma-installer/config"
|
||||
"miasma-installer/tui/styles"
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
@@ -19,6 +20,8 @@ type InstallModel struct {
|
||||
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 {
|
||||
@@ -33,8 +36,10 @@ func NewInstallModel(disk string, enableLUKS bool, hostname string, username str
|
||||
Timezone: "UTC",
|
||||
Locale: "en_US.UTF-8",
|
||||
},
|
||||
status: "Preparing installation...",
|
||||
progress: []string{},
|
||||
status: "Preparing installation...",
|
||||
progress: []string{},
|
||||
currentStep: 0,
|
||||
totalSteps: 8,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,34 +51,319 @@ type installCompleteMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type installProgressMsg struct {
|
||||
step int
|
||||
message string
|
||||
}
|
||||
|
||||
func (m InstallModel) startInstall() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
archConfig := m.config.ToArchInstall()
|
||||
|
||||
configPath := "/tmp/miasma-install-config.json"
|
||||
data, err := json.MarshalIndent(archConfig, "", " ")
|
||||
if err != nil {
|
||||
return installCompleteMsg{err: fmt.Errorf("failed to marshal config: %w", err)}
|
||||
if err := m.performInstall(); err != nil {
|
||||
return installCompleteMsg{err: err}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||
return installCompleteMsg{err: fmt.Errorf("failed to write config: %w", err)}
|
||||
}
|
||||
|
||||
cmd := exec.Command("archinstall", "--config", configPath)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return installCompleteMsg{err: fmt.Errorf("archinstall failed: %w\nOutput: %s", err, string(output))}
|
||||
}
|
||||
|
||||
if err := m.runPostInstallScripts(); err != nil {
|
||||
return installCompleteMsg{err: fmt.Errorf("post-install scripts failed: %w", 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)
|
||||
@@ -90,15 +380,30 @@ func (m InstallModel) runPostInstallScripts() error {
|
||||
}
|
||||
|
||||
scriptPath := filepath.Join(scriptsDir, entry.Name())
|
||||
cmd := exec.Command("/bin/bash", scriptPath)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("script %s failed: %w\nOutput: %s", entry.Name(), err, string(output))
|
||||
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:
|
||||
@@ -118,15 +423,30 @@ func (m InstallModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (m InstallModel) View() string {
|
||||
s := styles.TitleStyle.Render("Installing Miasma OS")
|
||||
s += "\n\n"
|
||||
s += m.status + "\n\n"
|
||||
s += fmt.Sprintf("Step %d/%d: %s\n\n", m.currentStep, m.totalSteps, m.status)
|
||||
|
||||
if m.err != nil {
|
||||
s += styles.HelpStyle.Render(fmt.Sprintf("Error: %v\n", m.err))
|
||||
}
|
||||
|
||||
if m.Finished && m.err == nil {
|
||||
s += styles.HelpStyle.Render("Press Ctrl+C to exit")
|
||||
s += "\n" + styles.HelpStyle.Render("Installation successful! You can now reboot.\nPress Ctrl+C to exit")
|
||||
}
|
||||
|
||||
return styles.MainStyle.Render(s)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user