From 27ca5a9426021755db589b3eb4a4e3fa64ec53c3 Mon Sep 17 00:00:00 2001 From: tumillanino Date: Tue, 28 Oct 2025 22:53:15 +1100 Subject: [PATCH] added installer shell script and additional steps --- .gitignore | 8 +++ Makefile | 46 +++++++++++++ README.md | 92 ++++++++++++++++++++++++++ config/config.go | 65 ++++++++++++++++++ install.sh | 36 ++++++++++ scripts/01-cosmic-setup.sh | 4 ++ scripts/02-hardening.sh | 4 ++ tui/steps/confirm.go | 71 ++++++++++++++++++++ tui/steps/encryption.go | 132 +++++++++++++++++++++++++++++++++++++ tui/steps/hostname.go | 61 +++++++++++++++++ tui/steps/user.go | 125 +++++++++++++++++++++++++++++++++++ 11 files changed, 644 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 config/config.go create mode 100755 install.sh create mode 100755 scripts/01-cosmic-setup.sh create mode 100755 scripts/02-hardening.sh create mode 100644 tui/steps/confirm.go create mode 100644 tui/steps/encryption.go create mode 100644 tui/steps/hostname.go create mode 100644 tui/steps/user.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15fac32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +build/ +miasma-installer +*.swp +*.swo +*~ +.DS_Store +CRUSH.md +.crush diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..064bd17 --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +.PHONY: build clean install release + +BINARY_NAME=miasma-installer +BUILD_DIR=build +VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +LDFLAGS=-ldflags "-X main.Version=$(VERSION)" + +build: + @echo "Building $(BINARY_NAME)..." + @mkdir -p $(BUILD_DIR) + go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) . + @echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)" + +clean: + @echo "Cleaning..." + @rm -rf $(BUILD_DIR) + @go clean + +install: build + @echo "Installing to /usr/local/bin..." + @sudo cp $(BUILD_DIR)/$(BINARY_NAME) /usr/local/bin/ + @sudo chmod +x /usr/local/bin/$(BINARY_NAME) + @echo "Installed successfully" + +release: + @echo "Building release binary..." + @mkdir -p $(BUILD_DIR) + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -a -installsuffix cgo -o $(BUILD_DIR)/$(BINARY_NAME) . + @strip $(BUILD_DIR)/$(BINARY_NAME) + @echo "Release build complete: $(BUILD_DIR)/$(BINARY_NAME)" + @ls -lh $(BUILD_DIR)/$(BINARY_NAME) + +run: build + @$(BUILD_DIR)/$(BINARY_NAME) + +test: + go test ./... + +fmt: + go fmt ./... + +vet: + go vet ./... + +tidy: + go mod tidy diff --git a/README.md b/README.md new file mode 100644 index 0000000..eef0538 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Miasma OS Installer + +Opinionated Arch Linux installer built with Go and Bubble Tea TUI framework. + +## Features + +- **Filesystem**: btrfs with optimized subvolume layout +- **Desktop**: Cosmic Desktop +- **Kernel**: linux-hardened +- **Encryption**: LUKS2 by default (optional) +- **Boot**: UEFI only (systemd-boot) +- **Future**: Secure Boot support planned + +## Quick Install + +Boot into Arch Linux installation media and run: + +```bash +curl -fsSL https://install.miasma-os.com | sh +``` + +Or manually: + +```bash +# Download and run installer +curl -fsSL https://github.com/miasma-os/miasma-installer/releases/latest/download/miasma-installer -o miasma-installer +chmod +x miasma-installer +sudo ./miasma-installer +``` + +## Development + +### Prerequisites + +- Go 1.25.2 or later +- Arch Linux (for testing) + +### Building + +```bash +# Build binary +make build + +# Build release binary (stripped, static) +make release + +# Install to /usr/local/bin +make install + +# Run directly +make run +``` + +### Testing + +```bash +# Run all tests +make test + +# Format code +make fmt + +# Lint +make vet +``` + +## Project Structure + +``` +. +├── config/ # Installation configuration and archinstall JSON generation +├── scripts/ # Post-install shell scripts +├── tui/ +│ ├── model.go # Root state machine +│ ├── steps/ # Installation step models +│ └── styles/ # Shared UI styles +├── install.sh # Bootstrap script for curl | sh installation +└── main.go # Entry point +``` + +## Post-Install Scripts + +Custom shell scripts in `scripts/` run after archinstall completes: + +- `01-cosmic-setup.sh` - Cosmic Desktop configuration +- `02-hardening.sh` - System hardening tweaks + +Scripts execute in alphabetical order. + +## License + +MIT diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..a7d5e06 --- /dev/null +++ b/config/config.go @@ -0,0 +1,65 @@ +package config + +type InstallConfig struct { + Disk string + EnableLUKS bool + Hostname string + Username string + RootPassword string + UserPassword string + Timezone string + Locale string +} + +type ArchInstallConfig struct { + Bootloader string `json:"bootloader"` + Kernels []string `json:"kernels"` + Filesystem string `json:"filesystem"` + Disk string `json:"disk"` + Encryption *Encryption `json:"encryption,omitempty"` + Hostname string `json:"hostname"` + Timezone string `json:"timezone"` + Locale string `json:"locale"` + Desktop string `json:"desktop"` + Users []User `json:"users"` +} + +type Encryption struct { + Type string `json:"type"` + Password string `json:"password"` +} + +type User struct { + Username string `json:"username"` + Password string `json:"password"` + Sudo bool `json:"sudo"` +} + +func (c *InstallConfig) ToArchInstall() *ArchInstallConfig { + ac := &ArchInstallConfig{ + Bootloader: "systemd-boot", + Kernels: []string{"linux-hardened"}, + Filesystem: "btrfs", + Disk: c.Disk, + Hostname: c.Hostname, + Timezone: c.Timezone, + Locale: c.Locale, + Desktop: "cosmic", + Users: []User{ + { + Username: c.Username, + Password: c.UserPassword, + Sudo: true, + }, + }, + } + + if c.EnableLUKS { + ac.Encryption = &Encryption{ + Type: "luks2", + Password: c.RootPassword, + } + } + + return ac +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..9372b31 --- /dev/null +++ b/install.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +INSTALL_URL="https://github.com/miasma-os/miasma-installer/releases/latest/download/miasma-installer" +INSTALL_PATH="/usr/local/bin/miasma-installer" + +echo "Miasma OS Installer" +echo "===================" +echo "" + +if [ "$EUID" -ne 0 ]; then + echo "Error: This installer must be run as root" + echo "Please run: sudo curl -fsSL https://install.miasma-os.com | sh" + exit 1 +fi + +if [ ! -d "/sys/firmware/efi" ]; then + echo "Error: UEFI boot mode not detected" + echo "Miasma OS requires UEFI boot mode" + exit 1 +fi + +if ! command -v archinstall &> /dev/null; then + echo "Installing archinstall..." + pacman -Sy --noconfirm archinstall +fi + +echo "Downloading Miasma installer..." +curl -fsSL "$INSTALL_URL" -o "$INSTALL_PATH" +chmod +x "$INSTALL_PATH" + +echo "" +echo "Starting Miasma OS installer..." +echo "" + +exec "$INSTALL_PATH" diff --git a/scripts/01-cosmic-setup.sh b/scripts/01-cosmic-setup.sh new file mode 100755 index 0000000..77fd9c1 --- /dev/null +++ b/scripts/01-cosmic-setup.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +echo "Setting up Cosmic Desktop..." diff --git a/scripts/02-hardening.sh b/scripts/02-hardening.sh new file mode 100755 index 0000000..dc6c909 --- /dev/null +++ b/scripts/02-hardening.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +echo "Applying system hardening..." diff --git a/tui/steps/confirm.go b/tui/steps/confirm.go new file mode 100644 index 0000000..4d1ce58 --- /dev/null +++ b/tui/steps/confirm.go @@ -0,0 +1,71 @@ +package steps + +import ( + "fmt" + "miasma-installer/tui/styles" + + tea "github.com/charmbracelet/bubbletea" +) + +type ConfirmModel struct { + Disk string + EnableLUKS bool + Hostname string + Username string + Confirmed bool + Finished bool +} + +func NewConfirmModel(disk string, enableLUKS bool, hostname string, username string) ConfirmModel { + return ConfirmModel{ + Disk: disk, + EnableLUKS: enableLUKS, + Hostname: hostname, + Username: username, + } +} + +func (m ConfirmModel) Init() tea.Cmd { + return nil +} + +func (m ConfirmModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if m.Finished { + return m, nil + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "y": + m.Confirmed = true + m.Finished = true + return m, nil + case "n": + m.Confirmed = false + m.Finished = true + return m, nil + } + } + + return m, nil +} + +func (m ConfirmModel) View() string { + s := styles.TitleStyle.Render("Confirm Installation") + s += "\n\n" + s += "Please review your installation settings:\n\n" + s += fmt.Sprintf(" Disk: %s\n", m.Disk) + s += fmt.Sprintf(" Encryption: %s\n", map[bool]string{true: "Enabled (LUKS2)", false: "Disabled"}[m.EnableLUKS]) + s += fmt.Sprintf(" Hostname: %s\n", m.Hostname) + s += fmt.Sprintf(" Username: %s\n", m.Username) + s += fmt.Sprintf(" Filesystem: btrfs\n") + s += fmt.Sprintf(" Desktop: Cosmic Desktop\n") + s += fmt.Sprintf(" Kernel: linux-hardened\n") + s += fmt.Sprintf(" Bootloader: systemd-boot (UEFI)\n") + s += "\n" + s += styles.HelpStyle.Render("WARNING: This will ERASE ALL DATA on ") + m.Disk + "\n\n" + s += styles.HelpStyle.Render("[y] Proceed with installation [n] Cancel") + + return styles.MainStyle.Render(s) +} diff --git a/tui/steps/encryption.go b/tui/steps/encryption.go new file mode 100644 index 0000000..87bc2ac --- /dev/null +++ b/tui/steps/encryption.go @@ -0,0 +1,132 @@ +package steps + +import ( + "miasma-installer/tui/styles" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +type EncryptionModel struct { + EnableEncryption bool + Password string + ConfirmPassword string + passwordInput textinput.Model + confirmInput textinput.Model + focusIndex int + Finished bool + showInputs bool +} + +func NewEncryptionModel() EncryptionModel { + pi := textinput.New() + pi.Placeholder = "Enter encryption password" + pi.EchoMode = textinput.EchoPassword + pi.Focus() + + ci := textinput.New() + ci.Placeholder = "Confirm password" + ci.EchoMode = textinput.EchoPassword + + return EncryptionModel{ + EnableEncryption: true, + passwordInput: pi, + confirmInput: ci, + showInputs: true, + } +} + +func (m EncryptionModel) Init() tea.Cmd { + return textinput.Blink +} + +func (m EncryptionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if m.Finished { + return m, nil + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "enter": + if !m.showInputs { + m.Finished = true + return m, nil + } + + if m.focusIndex == 0 { + m.focusIndex = 1 + m.passwordInput.Blur() + return m, m.confirmInput.Focus() + } else if m.focusIndex == 1 { + if m.passwordInput.Value() == m.confirmInput.Value() && m.passwordInput.Value() != "" { + m.Password = m.passwordInput.Value() + m.Finished = true + return m, nil + } + } + + case "y": + if !m.showInputs { + m.EnableEncryption = true + m.showInputs = true + return m, m.passwordInput.Focus() + } + + case "n": + if !m.showInputs { + m.EnableEncryption = false + m.Finished = true + return m, nil + } + + case "tab", "shift+tab": + if m.showInputs { + if m.focusIndex == 0 { + m.focusIndex = 1 + m.passwordInput.Blur() + return m, m.confirmInput.Focus() + } else { + m.focusIndex = 0 + m.confirmInput.Blur() + return m, m.passwordInput.Focus() + } + } + } + } + + var cmd tea.Cmd + if m.showInputs { + if m.focusIndex == 0 { + m.passwordInput, cmd = m.passwordInput.Update(msg) + } else { + m.confirmInput, cmd = m.confirmInput.Update(msg) + } + } + + return m, cmd +} + +func (m EncryptionModel) View() string { + s := styles.TitleStyle.Render("Disk Encryption") + s += "\n\n" + + if !m.showInputs { + s += "Enable LUKS2 disk encryption? (recommended)\n\n" + s += styles.HelpStyle.Render("[y] Yes [n] No") + } else { + s += "Enter encryption password:\n\n" + s += m.passwordInput.View() + "\n\n" + s += m.confirmInput.View() + "\n\n" + + if m.passwordInput.Value() != "" && m.confirmInput.Value() != "" { + if m.passwordInput.Value() != m.confirmInput.Value() { + s += styles.HelpStyle.Render("Passwords do not match\n") + } + } + + s += styles.HelpStyle.Render("[Tab] Switch fields [Enter] Continue") + } + + return styles.MainStyle.Render(s) +} diff --git a/tui/steps/hostname.go b/tui/steps/hostname.go new file mode 100644 index 0000000..d3bd44d --- /dev/null +++ b/tui/steps/hostname.go @@ -0,0 +1,61 @@ +package steps + +import ( + "miasma-installer/tui/styles" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +type HostnameModel struct { + Hostname string + input textinput.Model + Finished bool +} + +func NewHostnameModel() HostnameModel { + ti := textinput.New() + ti.Placeholder = "miasma" + ti.Focus() + ti.CharLimit = 63 + + return HostnameModel{ + input: ti, + } +} + +func (m HostnameModel) Init() tea.Cmd { + return textinput.Blink +} + +func (m HostnameModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if m.Finished { + return m, nil + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "enter": + if m.input.Value() != "" { + m.Hostname = m.input.Value() + m.Finished = true + return m, nil + } + } + } + + var cmd tea.Cmd + m.input, cmd = m.input.Update(msg) + return m, cmd +} + +func (m HostnameModel) View() string { + s := styles.TitleStyle.Render("System Hostname") + s += "\n\n" + s += "Enter a hostname for your system:\n\n" + s += m.input.View() + "\n\n" + s += styles.HelpStyle.Render("[Enter] Continue") + + return styles.MainStyle.Render(s) +} diff --git a/tui/steps/user.go b/tui/steps/user.go new file mode 100644 index 0000000..77601a1 --- /dev/null +++ b/tui/steps/user.go @@ -0,0 +1,125 @@ +package steps + +import ( + "miasma-installer/tui/styles" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +type UserModel struct { + Username string + Password string + usernameInput textinput.Model + passwordInput textinput.Model + confirmInput textinput.Model + focusIndex int + Finished bool +} + +func NewUserModel() UserModel { + ui := textinput.New() + ui.Placeholder = "username" + ui.Focus() + ui.CharLimit = 32 + + pi := textinput.New() + pi.Placeholder = "password" + pi.EchoMode = textinput.EchoPassword + + ci := textinput.New() + ci.Placeholder = "confirm password" + ci.EchoMode = textinput.EchoPassword + + return UserModel{ + usernameInput: ui, + passwordInput: pi, + confirmInput: ci, + } +} + +func (m UserModel) Init() tea.Cmd { + return textinput.Blink +} + +func (m UserModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if m.Finished { + return m, nil + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "enter": + if m.focusIndex == 0 && m.usernameInput.Value() != "" { + m.focusIndex = 1 + m.usernameInput.Blur() + return m, m.passwordInput.Focus() + } else if m.focusIndex == 1 && m.passwordInput.Value() != "" { + m.focusIndex = 2 + m.passwordInput.Blur() + return m, m.confirmInput.Focus() + } else if m.focusIndex == 2 { + if m.passwordInput.Value() == m.confirmInput.Value() && m.passwordInput.Value() != "" { + m.Username = m.usernameInput.Value() + m.Password = m.passwordInput.Value() + m.Finished = true + return m, nil + } + } + + case "tab", "shift+tab": + m.focusIndex = (m.focusIndex + 1) % 3 + cmds := make([]tea.Cmd, 3) + + if m.focusIndex == 0 { + cmds[0] = m.usernameInput.Focus() + m.passwordInput.Blur() + m.confirmInput.Blur() + } else if m.focusIndex == 1 { + cmds[1] = m.passwordInput.Focus() + m.usernameInput.Blur() + m.confirmInput.Blur() + } else { + cmds[2] = m.confirmInput.Focus() + m.usernameInput.Blur() + m.passwordInput.Blur() + } + + return m, tea.Batch(cmds...) + } + } + + var cmd tea.Cmd + if m.focusIndex == 0 { + m.usernameInput, cmd = m.usernameInput.Update(msg) + } else if m.focusIndex == 1 { + m.passwordInput, cmd = m.passwordInput.Update(msg) + } else { + m.confirmInput, cmd = m.confirmInput.Update(msg) + } + + return m, cmd +} + +func (m UserModel) View() string { + s := styles.TitleStyle.Render("User Account") + s += "\n\n" + s += "Create your user account:\n\n" + s += "Username:\n" + s += m.usernameInput.View() + "\n\n" + s += "Password:\n" + s += m.passwordInput.View() + "\n\n" + s += "Confirm Password:\n" + s += m.confirmInput.View() + "\n\n" + + if m.passwordInput.Value() != "" && m.confirmInput.Value() != "" { + if m.passwordInput.Value() != m.confirmInput.Value() { + s += styles.HelpStyle.Render("Passwords do not match\n") + } + } + + s += styles.HelpStyle.Render("[Tab] Switch fields [Enter] Continue") + + return styles.MainStyle.Render(s) +}