added installer shell script and additional steps

This commit is contained in:
tumillanino
2025-10-28 22:53:15 +11:00
parent 6f1a6f6efa
commit 27ca5a9426
11 changed files with 644 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
build/
miasma-installer
*.swp
*.swo
*~
.DS_Store
CRUSH.md
.crush

46
Makefile Normal file
View File

@@ -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

92
README.md Normal file
View File

@@ -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

65
config/config.go Normal file
View File

@@ -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
}

36
install.sh Executable file
View File

@@ -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"

4
scripts/01-cosmic-setup.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -e
echo "Setting up Cosmic Desktop..."

4
scripts/02-hardening.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -e
echo "Applying system hardening..."

71
tui/steps/confirm.go Normal file
View File

@@ -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)
}

132
tui/steps/encryption.go Normal file
View File

@@ -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)
}

61
tui/steps/hostname.go Normal file
View File

@@ -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)
}

125
tui/steps/user.go Normal file
View File

@@ -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)
}