diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index c5dac56..e987c8f 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -32,47 +32,8 @@ jobs:
- name: Evaluate Makima Host Configuration
run: nix eval --impure --expr '(builtins.getFlake (toString ./.)).nixosConfigurations.Makima.config.system.build.toplevel.name'
- rust-ci:
- name: Rust TUI Installer Tests & Build
- runs-on: ubuntu-latest
- defaults:
- run:
- working-directory: installer-rs
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Rust
- uses: dtolnay/rust-toolchain@stable
- with:
- components: rustfmt, clippy
-
- - name: Cache cargo
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/registry
- ~/.cargo/git
- installer-rs/target
- key: ${{ runner.os }}-cargo-${{ hashFiles('installer-rs/Cargo.lock') }}
-
- name: Check formatting
- run: cargo fmt --check
-
- - name: Clippy (lint)
- run: cargo clippy -- -D warnings
-
- - name: Run Rust unit & integration tests
- run: cargo test --verbose
-
- - name: Build binary
- run: cargo build --release
-
- - name: Verify Cargo.lock is up to date
- run: |
- cargo update --dry-run 2>&1
- echo "✅ Cargo.lock is up to date"
+ run: nix fmt -- --check .
python-ci:
name: Python Installer & E2E Tests
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
deleted file mode 100644
index 151a58f..0000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,79 +0,0 @@
-name: Release Installer
-
-on:
- push:
- tags:
- - 'v*'
-
-permissions:
- contents: write
-
-jobs:
- build-rust:
- name: Build Rust Installer
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Rust
- uses: dtolnay/rust-toolchain@stable
-
- - name: Cache cargo
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/registry
- ~/.cargo/git
- installer-rs/target
- key: ${{ runner.os }}-cargo-release-${{ hashFiles('installer-rs/Cargo.lock') }}
-
- - name: Populate embedded flake directory
- run: |
- rm -rf installer-rs/flake/*
- cp flake.nix installer-rs/flake/
- cp flake.lock installer-rs/flake/
- cp README.md installer-rs/flake/
- cp -r hosts installer-rs/flake/hosts
- cp -r home installer-rs/flake/home
- cp -r modules installer-rs/flake/modules
- cp -r lib installer-rs/flake/lib
- cp -r flake installer-rs/flake/flake
- cp -r assets installer-rs/flake/assets
-
- - name: Run Tests
- working-directory: installer-rs
- run: cargo test
-
- - name: Build Release Binary
- working-directory: installer-rs
- run: cargo build --release
-
- - name: Rename binary
- run: cp installer-rs/target/release/northstar-installer installer-rs/northstar-installer
-
- - name: Upload artifact
- uses: actions/upload-artifact@v4
- with:
- name: rust-binary
- path: installer-rs/northstar-installer
-
- release:
- name: Create Release
- needs: [build-rust]
- runs-on: ubuntu-latest
-
- steps:
- - name: Download Rust binary
- uses: actions/download-artifact@v4
- with:
- name: rust-binary
-
- - name: Create Release
- uses: softprops/action-gh-release@v2
- with:
- generate_release_notes: true
- files: |
- northstar-installer
- fail_on_unmatched_files: true
diff --git a/.gitignore b/.gitignore
index 572612f..580fe1a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
-result
-installer-rs/target/
+result/
__pycache__/
*.pyc
+.direnv/
-assets/wallpapers/Einsamer_Raum.jpg
+# Ignore all wallpapers except rose pine fractal
+assets/wallpapers/*
+!assets/wallpapers/rose-pine-fractal.jpg
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..6fd5a44
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,125 @@
+# Contributing to Northstar
+
+Thanks for your interest in contributing! This guide covers the development workflow.
+
+## Prerequisites
+
+- [Nix](https://nixos.org/download.html) with flakes enabled
+- Add to `~/.config/nix/nix.conf`:
+ ```
+ experimental-features = nix-command flakes pipe-operators
+ ```
+
+## Development Shell
+
+Enter the dev shell for formatting, linting, and test tools:
+
+```bash
+nix develop
+```
+
+This provides `nixfmt`, `nil` (Nix LSP), `python3`, `git`, and `jq`.
+
+## Adding a New Host
+
+1. Run the interactive installer (creates `hosts/` automatically):
+ ```bash
+ nix run . --impure
+ ```
+
+2. Or create a host directory manually (`hosts/YourHostName` containing `default.nix`, `disko.nix`, and `hardware.nix`):
+ ```bash
+ mkdir -p hosts/YourHostName
+ sudo nixos-generate-config --show-hardware-config > hosts/YourHostName/hardware.nix
+ ```
+
+3. Edit `hosts/YourHostName/default.nix` — set your username, profiles, GPU settings, etc.
+
+4. Configure your disk layout in `hosts/YourHostName/disko.nix` using `mkDisko`.
+
+5. Build and switch:
+ ```bash
+ sudo nixos-rebuild switch --flake .#YourHostName
+ ```
+
+The host is **auto-discovered** — any directory in `hosts/` with both `default.nix` and `hardware.nix` is automatically registered as a NixOS configuration.
+
+## Adding a New Feature Module
+
+1. Create a new `.nix` file in the appropriate `modules/features//` directory.
+
+2. Follow the standard pattern:
+ ```nix
+ { config, lib, ... }:
+ let
+ cfg = config.northstar.features.yourfeature;
+ in
+ {
+ options.northstar.features.yourfeature.enable =
+ lib.mkEnableOption "description of your feature";
+
+ config = lib.mkIf cfg.enable {
+ # Your configuration here
+ };
+ }
+ ```
+
+3. The module is **auto-discovered** — `lib.scanModules` picks up any `.nix` file in `modules/` (except `default.nix` and files starting with `.` or `_`).
+
+4. Optionally add it to a profile in `modules/profiles/`.
+
+## Running Tests
+
+```bash
+# Nix-native checks (module syntax, lib tests, eval tests)
+nix flake check --impure
+
+# Python installer tests
+python3 -m unittest discover -s tests -v
+
+# Format check
+nix fmt -- --check .
+```
+
+## Code Formatting
+
+Format all Nix files:
+
+```bash
+nix fmt
+```
+
+We use `nixfmt-rfc-style` (the RFC 166 formatter).
+
+## PR Workflow
+
+1. Create a feature branch: `git checkout -b feat/your-feature`
+2. Make your changes
+3. Run `nix fmt` to format
+4. Run `nix flake check --impure` to verify
+5. Commit and push
+6. Open a PR against `main`
+
+## Directory Structure
+
+```
+northstar/
+├── flake.nix # Flake entry point
+├── flake/ # flake-parts modules (hosts, installer, checks, devshell, formatter)
+├── hosts/ # Host machine configs (auto-discovered)
+│ ├── common.nix # Shared base config
+│ └── / # Per-machine config
+├── home/
+│ └── home.nix # Base Home Manager user environment
+├── lib/
+│ ├── core.nix # Shared Nix helpers (scanModules, discoverHosts, mkDisko, mkUser, mkProfile)
+│ └── disko/
+│ └── generator.nix # Dynamic mkDisko partition generator
+├── modules/ # Option-based modules (auto-discovered)
+│ ├── features/ # Vertical feature slices (core, desktop, dev, shell, terminals, tools)
+│ ├── hardware/ # Hardware drivers (NVIDIA)
+│ └── profiles/ # Composable feature bundles (base, desktop, workstation, gaming)
+├── installer/ # Python interactive installer
+├── tests/ # Python test suites
+└── assets/ # Themes and media
+```
diff --git a/README.md b/README.md
index db72762..0a58aaa 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,10 @@
-❄️ Northstar-nix
+❄️ Northstar
- A premium, modular, option-driven NixOS & Home Manager configuration built on
+ A modular, option-driven NixOS & Home Manager configuration built on
flake-parts,
Home Manager,
disko, and modern
@@ -16,7 +16,7 @@
-
+
@@ -24,305 +24,212 @@
## ✨ Key Features
-- **🎛️ Toggle-based modularity** — every system and user module is cleanly isolated behind `northstar...enable` options.
-- **⚡ Functional Pipe Operator Composition** — leverages native experimental `pipe-operators` (`|>`) for clean functional transformations in profile bundles and discovery helpers.
-- **📁 Auto-discovered configurations** — drop a host directory in `hosts/` or a module anywhere in `modules/` and it is dynamically detected, scanned, and wired up via `lib.scanModules`.
-- **🛡️ UEFI Secure Boot & Configurable Bootloaders** — first-class support for **Lanzaboote Secure Boot** (`sbctl`), **Limine** (ultra-fast modern UEFI bootloader), and **GRUB 2** styled with the cyber DedSec theme.
-- **🤖 Dedicated AI/ML & Gaming Workstations** — out-of-the-box modules for local LLMs (Ollama, PyTorch, Llama.cpp with CUDA/ROCm acceleration) and high-performance gaming (Steam, Gamemode, Gamescope, MangoHud, Wine/Proton, and controller drivers).
-- **🛠️ Automated USB Auto-mounting** — seamless external device detection, auto-mounting, and desktop notifications via `udisks2`, `gvfs`, and `udiskie`.
-- **💻 Decoupled Development Workspaces** — developer toolchains (compilers, LSPs, direnv, git, container runtimes, virtualization) isolated into clean workstation profiles.
-- **🚀 Dual Async Installers** — both an interactive Ratatui TUI installer (`installer-rs`) with live hardware detection & split log viewing, and an automated Python installer (`installer/install.py`).
-- **💾 Declarative Dual-Boot & Disko** — supports whole-disk formatting or partition-only dual-boot layouts with Btrfs subvolumes (`/root`, `/home`, `/nix`, `/var/log`, `/swap`) and auto-chainloading for Windows, Fedora, Ubuntu, and Arch.
-- **🔄 Idempotency & Checkpointing** — execution checkpoints allow resuming interrupted installations seamlessly.
-
----
-
-## 📂 Directory Structure
-
-The repository is organized into distinct domain layers:
-
-```
-northstar/
-├── flake.nix # Flake entry point (inputs, flake-parts wire-up)
-├── flake.lock
-│
-├── flake/ # flake-parts modules
-│ ├── hosts.nix # Host discovery & nixosConfigurations exports
-│ ├── installer.nix # Python installer package definition
-│ └── rust-installer.nix # Rust TUI installer package definition (crane)
-│
-├── hosts/ # Host machine configurations
-│ ├── common.nix # Shared system base config (flakes, pipe-operators)
-│ └── / # Per-machine system settings & hardware declarations
-│ ├── default.nix # Host entry point (users, profiles, bootloader, GPU)
-│ ├── disko.nix # Declarative partition scheme
-│ └── hardware.nix # Hardware scans (kernel modules, CPU microcode)
-│
-├── lib/ # Shared Nix helpers and discovery templates
-│ ├── default.nix # Module discovery (lib.scanModules), host discovery
-│ └── disko/
-│ ├── btrfs.nix # Whole-disk Btrfs layout with subvolumes
-│ └── ext4.nix # Whole-disk Ext4 layout
-│
-├── home/
-│ └── default.nix # Home Manager configuration entry point
-│
-├── modules/ # Option-based module definitions
-│ ├── features/ # Vertical feature slices
-│ │ ├── core/ # Boot, env, fonts, locale, networking, packages, shells
-│ │ ├── desktop/ # Audio, display, browsers, Hyprland, Niri, Noctalia, Caelestia, Gaming
-│ │ ├── development/ # Dev defaults, toolchains, AI/ML, git, Emacs, virtualization
-│ │ ├── shell/ # Fish, Zsh, Starship, Oh My Posh
-│ │ ├── terminals/ # Ghostty, Kitty
-│ │ └── tools/ # Eza, Fzf, Tmux, Yazi, Zoxide
-│ ├── hardware/ # Hardware drivers (NVIDIA, Prime)
-│ └── profiles/ # Composable feature bundles (base, desktop, workstation, gaming)
-│
-├── installer-rs/ # Ratatui TUI async installer source (Rust)
-│ ├── src/ # App state, hardware detection, UI widgets, Disko generators
-│ └── tests/ # Automated unit test suite (21 tests)
-├── installer/ # Python interactive installer source
-├── tests/ # Python installer & E2E verification test suite (167 tests)
-└── assets/ # DedSec Plymouth splash, themes, and media assets
-```
+- **🎛️ Toggle-based modularity** — every module is behind `northstar...enable` options
+- **⚡ Pipe Operator Composition** — leverages native `pipe-operators` (`|>`) for clean functional transformations
+- **📁 Auto-discovery** — drop a host directory in `hosts/` or a module in `modules/` and it's automatically wired up
+- **🛡️ UEFI Secure Boot** — native Secure Boot via Limine as the sole UEFI bootloader
+- **💾 Dynamic Disko** — `mkDisko` generates partition configs for whole-disk or dual-boot layouts with Btrfs/Ext4
+- **🤖 AI/ML & Gaming** — out-of-the-box modules for Ollama, PyTorch, CUDA/ROCm, Steam, Gamescope, and more
+- **🔐 Secrets Management** — integrated sops-nix & age key management
+- **🚀 Interactive Installer** — Python-based installer with hardware detection and guided setup
---
## 🚀 Quick Start
-### 1. Installation from a NixOS Live USB
-
-Before running the installers, export the flake and experimental feature flags (including **pipe operators**):
+### Fresh Install from NixOS Live USB
```bash
+# Enable experimental features
export NIX_CONFIG="experimental-features = nix-command flakes pipe-operators"
-```
-
-> [!TIP]
-> **Option A (Recommended): Pre-built Rust TUI Installer Binary**
-> ```bash
-> curl -fsSL https://github.com/reze-dev/northstar/releases/latest/download/northstar-installer -o installer
-> chmod +x installer && sudo ./installer
-> ```
-
-> [!NOTE]
-> **Option B: Run Rust Installer via Nix**
-> ```bash
-> nix run github:reze-dev/northstar#rust-install --impure
-> ```
-
-> [!NOTE]
-> **Option C: Run Python Installer via Nix**
-> ```bash
-> nix run github:reze-dev/northstar --impure
-> ```
-
----
-### 2. Post-installation System Management
+# Run the installer
+nix run github:reze-dev/northstar --impure
+```
-To apply configuration changes on an installed system:
+### Adding a Host
```bash
+# Clone the repo locally
+git clone https://github.com/reze-dev/northstar ~/northstar
cd ~/northstar
+
+# Run the interactive installer (creates hosts/ automatically)
+nix run . --impure
+
+# Build and switch to your host
sudo nixos-rebuild switch --flake .#
```
-For example, for the `Makima` host:
-```bash
-sudo nixos-rebuild switch --flake .#Makima
-```
+See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide on adding hosts and modules.
+
+### Day-to-Day Workflow
-To update flake inputs and dependencies:
```bash
+# Apply configuration changes
+sudo nixos-rebuild switch --flake .#
+
+# Update all flake inputs
nix flake update
+
+# Format code
+nix fmt
+
+# Run checks
+nix flake check --impure
+```
+
+---
+
+## 📂 Directory Structure
+
+```
+northstar/
+├── flake.nix # Flake entry point (inputs + flake-parts wire-up)
+├── flake/ # flake-parts modules
+│ ├── hosts.nix # Host discovery & nixosConfigurations
+│ ├── installer.nix # Python installer package
+│ ├── checks.nix # Nix-native test checks
+│ ├── devshell.nix # Development shell
+│ └── formatter.nix # Code formatter (nixfmt-rfc-style)
+│
+├── hosts/ # Host machine configurations (auto-discovered)
+│ ├── common.nix # Shared base config (flakes, HM, nix-index)
+│ └── / # Per-machine config
+│ ├── default.nix # Host entry point (users, profiles, GPU)
+│ ├── disko.nix # Declarative partition scheme
+│ └── hardware.nix # Hardware scan (from nixos-generate-config)
+│
+├── lib/ # Shared Nix helpers
+│ ├── core.nix # scanModules, discoverHosts, mkProfile, mkUser, mkDisko
+│ └── disko/
+│ └── generator.nix # Dynamic mkDisko partition generator
+│
+├── home/
+│ └── home.nix # Base Home Manager user environment
+│
+├── modules/ # Option-based modules (auto-discovered)
+│ ├── features/ # Vertical feature slices
+│ │ ├── core/ # Boot, env, fonts, locale, networking, packages, shells, secrets
+│ │ ├── desktop/ # Audio, display, browsers, Hyprland, Niri, Noctalia, Gaming, desktop packages
+│ │ ├── development/ # Dev tools, AI/ML, git, Emacs, virtualization
+│ │ ├── shell/ # Fish, Zsh, Starship
+│ │ ├── terminals/ # Ghostty, Kitty
+│ │ └── tools/ # Eza, Fzf, Tmux, Yazi, Zoxide
+│ ├── hardware/ # Hardware drivers (NVIDIA, Prime)
+│ └── profiles/ # Composable feature bundles
+│
+├── installer/ # Python interactive installer
+├── tests/ # Python test suites
+└── assets/ # DedSec Plymouth splash, wallpapers
```
---
## 🎛️ Profiles & Feature Bundles
-Northstar uses clean profile bundles composed via the native Nix **pipe operator** (`|>`):
+Profiles are composable feature bundles. Enable them in your host config:
```nix
-# Example from modules/profiles/desktop.nix
-northstar.features = features |> (f: lib.genAttrs f (_: { enable = true; }));
+northstar.profiles = {
+ desktop.enable = true;
+ workstation.enable = true;
+};
```
-### Available Profiles (`northstar.profiles.*`)
-
-| Profile | Option | Description |
-| :--- | :--- | :--- |
-| **Base** | `northstar.profiles.base.enable` | Minimal headless/server system (boot, networking, SSH, neovim, shells, fonts, locales, env) |
-| **Desktop** | `northstar.profiles.desktop.enable` | Full graphical workstation (Audio, Bluetooth, Hyprland, Niri, Noctalia, Firefox, Zen, Ghostty, Kitty, XDG, Power, Udiskie) |
-| **Workstation** | `northstar.profiles.workstation.enable` | Desktop + Full Developer Toolchains, Docker/Libvirt virtualization, AI/ML SDKs, Emacs, and Direnv |
-| **Gaming** | `northstar.profiles.gaming.enable` | Dedicated gaming environment (Steam, Gamemode, Gamescope, MangoHud, Wine/Proton, Lutris, latency tweaks, controllers) |
+| Profile | Description |
+| :--- | :--- |
+| **Base** | Minimal system (boot, networking, SSH, neovim, shells, fonts, locales) — always enabled |
+| **Desktop** | Full graphical workstation (Audio, Bluetooth, Hyprland, Niri, Noctalia, browsers, etc.) |
+| **Workstation** | Developer tools, shells, editors, containers, virtualization |
+| **Gaming** | Steam, Gamemode, Gamescope, MangoHud, Wine/Proton, Lutris, controllers |
---
## 🔧 Module Reference
-### Vertical Feature Modules (`northstar.features.*`)
-
-#### Core & System
-| Module | Option | Description |
-| :--- | :--- | :--- |
-| **Boot** | `northstar.features.boot.enable` | Configurable bootloader (`grub` or `limine`) and Plymouth splash |
-| **Bootloader** | `northstar.features.boot.loader` | Choose `"grub"` (with DedSec cyberpunk theme) or `"limine"` (fast modern UEFI) |
-| **Secure Boot**| `northstar.features.boot.secureBoot.enable` | Lanzaboote UEFI Secure Boot integration + `sbctl` package |
-| **Networking**| `northstar.features.networking.enable` | NetworkManager daemon + customized firewall settings |
-| **Locales** | `northstar.features.locales.enable` | Timezone, keyboard layout, and i18n locales |
-| **Fonts** | `northstar.features.fonts.enable` | Curated Nerd Fonts collection |
-| **Packages** | `northstar.features.packages.enable` | Base system utilities, `udisks2`, `gvfs`, and core CLI tools |
-| **SSH** | `northstar.features.ssh.enable` | OpenSSH daemon |
-| **Environment**| `northstar.features.env.enable` | Global environment variables (`EDITOR`, `VISUAL`, etc.) |
-
-#### Desktop & Window Managers
-| Module | Option | Description |
-| :--- | :--- | :--- |
-| **Hyprland** | `northstar.features.hyprland.enable` | Dynamic tiling Wayland compositor with animations & keybinds |
-| **Niri** | `northstar.features.niri.enable` | Scrollable-tiling Wayland compositor |
-| **Noctalia** | `northstar.features.noctalia.enable` | Noctalia Wayland desktop shell |
-| **Caelestia** | `northstar.features.caelestia.enable` | Caelestia desktop shell integration |
-| **Display** | `northstar.features.display.enable` | Greetd login manager with `tuigreet` |
-| **Audio** | `northstar.features.audio.enable` | PipeWire audio stack, Low-latency ALSA, and PulseAudio emu |
-| **Bluetooth** | `northstar.features.bluetooth.enable` | BlueZ daemon + Blueman applet |
-| **Firefox** | `northstar.features.firefox.enable` | Firefox web browser |
-| **Zen Browser**| `northstar.features.zen-browser.enable` | Zen Browser flake integration |
-| **Gaming** | `northstar.features.desktop.gaming.enable` | Steam, Gamemode, Gamescope, MangoHud, Wine, Lutris, controllers |
-| **Power** | `northstar.features.power.enable` | UPower daemon + power-profiles-daemon |
-| **udiskie** | `northstar.features.udiskie.enable` | Auto-mounting daemon for removable media & USB drives |
-| **CUPS** | `northstar.features.cups.enable` | Printing support via CUPS daemon |
-
-#### Development & AI/ML
-| Module | Option | Description |
-| :--- | :--- | :--- |
-| **AI / ML** | `northstar.features.development.aiml.enable` | Ollama LLM server, PyTorch, Llama.cpp, JupyterLab, CUDA/ROCm SDKs |
-| **DevTools** | `northstar.features.devtools.enable` | Compilers (GCC/Clang/Go/Rustup/Zig/JDK/Haskell) and LSPs |
-| **Dev Defaults**| `northstar.features.dev.enable` | System-wide direnv, git, gpg, and `nix-ld` configuration |
-| **Virtualization**| `northstar.features.virtualization.enable` | Docker, Libvirtd, QEMU/KVM, and Virt-Manager |
-| **Emacs** | `northstar.features.emacs.enable` | Emacs daemon running under systemd |
-| **Neovim** | `northstar.features.neovim.enable` | Curated Neovim editor configuration |
-| **direnv** | `northstar.features.direnv.enable` | Per-directory automatic shell environments |
-| **Git** | `northstar.features.git.enable` | Git configuration, aliases, and signing keys |
-
-#### Shells, Terminals & CLI Tools
-| Module | Option | Description |
-| :--- | :--- | :--- |
-| **Zsh** | `northstar.features.zsh.enable` | Zsh shell with plugins, autocompletions, and syntax highlighting |
-| **Fish** | `northstar.features.fish.enable` | Fish shell with friendly prompt integrations |
-| **Ghostty** | `northstar.features.ghostty.enable` | Ghostty GPU-accelerated terminal emulator |
-| **Kitty** | `northstar.features.kitty.enable` | Kitty GPU-accelerated terminal emulator |
-| **Starship** | `northstar.features.starship.enable` | Starship fast cross-shell prompt |
-| **Oh My Posh**| `northstar.features.omp.enable` | Oh My Posh prompt theming engine |
-| **Tmux** | `northstar.features.tmux.enable` | Tmux multiplexer with `tmux-powerkit` theme |
-| **Yazi** | `northstar.features.yazi.enable` | Fast terminal file manager with image previews |
-| **Eza** | `northstar.features.eza.enable` | Modern `ls` replacement with git status & icons |
-| **Fzf** | `northstar.features.fzf.enable` | Fuzzy finder CLI integrations |
-| **Zoxide** | `northstar.features.zoxide.enable` | Smarter `cd` command with memory-based navigation |
-
-#### Hardware Acceleration & GPUs
-| Module | Option | Description |
-| :--- | :--- | :--- |
-| **NVIDIA** | `northstar.nvidia.enable` | Proprietary NVIDIA drivers & CUDA support |
-| **NVIDIA Prime**| `northstar.nvidia.prime.enable` | Hybrid graphics offload (NVIDIA discrete + Intel/AMD integrated) |
-| **Prime Bus IDs**| `northstar.nvidia.prime.nvidiaBusId` / `intelBusId` / `amdgpuBusId` | PCI bus identifiers (auto-formatted as `PCI:X:Y:Z`) |
+### Core & System (`northstar.features.*`)
+| Module | Description |
+| :--- | :--- |
+| `boot` | Bootloader (Limine), Plymouth splash, Secure Boot (Limine) |
+| `networking` | NetworkManager + configurable `/etc/hosts` |
+| `locales` | Timezone, keyboard, i18n |
+| `fonts` | Curated Nerd Fonts |
+| `packages` | Base system utilities |
+| `ssh` | OpenSSH daemon |
+| `env` | Environment variables (EDITOR, VISUAL, BROWSER) |
+| `secrets` | sops-nix & age secret management |
+
+### Desktop (`northstar.features.*`)
+| Module | Description |
+| :--- | :--- |
+| `hyprland` | Dynamic tiling Wayland compositor |
+| `niri` | Scrollable-tiling Wayland compositor |
+| `noctalia` | Noctalia Wayland shell |
+| `caelestia` | Caelestia desktop shell |
+| `display` | Greetd login manager with tuigreet |
+| `audio` | PipeWire audio stack |
+| `bluetooth` | BlueZ + Blueman |
+| `firefox` / `zen-browser` | Web browsers |
+| `gaming` | Steam, Gamemode, Gamescope, Wine, Lutris |
+| `xdg` | XDG portals & MIME associations |
+| `desktopPackages` | Wayland/GUI desktop utilities (polkit, clipboard, screenshot, file manager) |
+
+### Development (`northstar.features.*`)
+| Module | Description |
+| :--- | :--- |
+| `dev` | direnv, git, gpg, nix-ld |
+| `devtools` | Compilers (GCC, Clang, Go, Rust, Zig, JDK, Haskell) & LSPs |
+| `development.aiml` | Ollama, PyTorch, Llama.cpp, JupyterLab, CUDA/ROCm |
+| `virtualization` | Docker, Libvirtd, QEMU/KVM |
+| `emacs` | Emacs daemon |
+
+### Hardware (`northstar.nvidia.*`)
+| Option | Description |
+| :--- | :--- |
+| `enable` | Proprietary NVIDIA drivers |
+| `prime.enable` | Hybrid GPU (NVIDIA + Intel/AMD) |
+| `prime.nvidiaBusId` / `intelBusId` / `amdgpuBusId` | PCI bus IDs |
---
-## 🛠️ Customizing a Host Configuration
+## 💾 Disk Configuration (mkDisko)
-Here is an example host configuration (`hosts/Makima/default.nix`):
+Northstar provides `mkDisko` for declarative disk layouts:
```nix
-{
- config,
- lib,
- pkgs,
- ...
-}:
-
-{
- imports = [
- ./disko.nix
- ];
-
- home-manager.users.reze = {
- imports = [ ../../home ];
- home.username = lib.mkForce "reze";
- home.homeDirectory = lib.mkForce "/home/reze";
- };
-
- # 1. Bootloader Selection (GRUB with DedSec theme, or modern Limine)
- northstar.features.boot.loader = "grub"; # or "limine"
-
- # Optional: Dual-boot chainloader entries
- boot.loader.grub.extraEntries = ''
- menuentry "Fedora Linux" {
- search --fs-uuid --set=root CB41-6695
- chainloader /EFI/fedora/shimx64.efi
- }
- '';
-
- # 2. User Accounts
- users.users.reze = {
- isNormalUser = true;
- description = "reze";
- extraGroups = [ "networkmanager" "wheel" "libvirtd" "docker" ];
- shell = pkgs.zsh;
- hashedPassword = "$6$example..."; # Generated via mkpasswd -m sha-512
- };
-
- # 3. Northstar Profile Presets
- northstar.profiles = {
- desktop.enable = true;
- workstation.enable = true;
- gaming.enable = true;
- };
-
- # 4. Granular Feature Overrides (Optional)
- northstar.features = {
- hyprland.enable = true;
- ghostty.enable = true;
- development.aiml.enable = true;
- };
-
- # 5. Hybrid GPU (NVIDIA Prime)
- northstar.nvidia.enable = true;
- northstar.nvidia.prime = {
- enable = true;
- nvidiaBusId = "PCI:1:0:0";
- amdgpuBusId = "PCI:5:0:0";
- };
-
- networking.hostName = "Makima";
- system.stateVersion = "26.11";
+# Whole-disk (wipes entire drive)
+northstar.mkDisko {
+ mode = "whole-disk";
+ device = "/dev/nvme0n1";
+ fsType = "btrfs"; # or "ext4"
+ efiSize = "2G"; # 2GB default — safe for Limine + many NixOS generations
+ swapSize = "16G"; # default 16G — "0" to disable
+}
+
+# Partition-only (dual-boot safe)
+northstar.mkDisko {
+ mode = "partition-only";
+ nixosPart = "/dev/nvme0n1p4";
+ efiDevice = "/dev/disk/by-uuid/XXXX-XXXX";
+ fsType = "btrfs";
+ swapSize = "16G";
}
```
---
-## 🧪 Testing & Quality Assurance
-
-Both the Rust TUI installer and Python installer are backed by automated unit and integration tests:
+## 🧪 Testing
-### Running the Rust Test Suite (21 Tests)
```bash
-cargo test --manifest-path installer-rs/Cargo.toml
-```
+# Nix-native checks (module syntax, lib tests, eval tests)
+nix flake check --impure
-### Running the Python Test Suite (167 Tests)
-```bash
-python3 -m unittest discover tests
-```
+# Python installer tests
+python3 -m unittest discover -s tests -v
-### Validating Flake Builds & Nix Evaluations
-```bash
-# Evaluate top-level system generation
+# Evaluate host configuration
nix eval --impure .#nixosConfigurations.Makima.config.system.build.toplevel.name
-
-# Build the Rust installer package via Crane
-nix build --impure .#rust-installer --no-link
```
---
diff --git a/assets/wallpapers/11356475.png b/assets/wallpapers/11356475.png
deleted file mode 100644
index ccf2a7b..0000000
Binary files a/assets/wallpapers/11356475.png and /dev/null differ
diff --git a/assets/wallpapers/Arizona_night.jpg b/assets/wallpapers/Arizona_night.jpg
deleted file mode 100755
index a7a7bf2..0000000
Binary files a/assets/wallpapers/Arizona_night.jpg and /dev/null differ
diff --git a/assets/wallpapers/Konachan.com - 367007 all_male animal animated bird cigarette clouds lennsan male original reflection sky smoking tree water.gif b/assets/wallpapers/Konachan.com - 367007 all_male animal animated bird cigarette clouds lennsan male original reflection sky smoking tree water.gif
deleted file mode 100755
index fb37154..0000000
Binary files a/assets/wallpapers/Konachan.com - 367007 all_male animal animated bird cigarette clouds lennsan male original reflection sky smoking tree water.gif and /dev/null differ
diff --git a/assets/wallpapers/Konachan.com - 367008 animated building city clouds landscape lennsan no_humans original petals scenic sky sunset tree.gif b/assets/wallpapers/Konachan.com - 367008 animated building city clouds landscape lennsan no_humans original petals scenic sky sunset tree.gif
deleted file mode 100755
index 28e6fd9..0000000
Binary files a/assets/wallpapers/Konachan.com - 367008 animated building city clouds landscape lennsan no_humans original petals scenic sky sunset tree.gif and /dev/null differ
diff --git a/assets/wallpapers/Konachan.com - 367009 animal animated bird building city clouds flowers lennsan no_humans original petals polychromatic reflection scenic sky train tree water.gif b/assets/wallpapers/Konachan.com - 367009 animal animated bird building city clouds flowers lennsan no_humans original petals polychromatic reflection scenic sky train tree water.gif
deleted file mode 100755
index b9c0579..0000000
Binary files a/assets/wallpapers/Konachan.com - 367009 animal animated bird building city clouds flowers lennsan no_humans original petals polychromatic reflection scenic sky train tree water.gif and /dev/null differ
diff --git a/assets/wallpapers/Konachan.com - 367010 animated autumn building clouds leaves lennsan no_humans original scenic shrine sky sunset tree.gif b/assets/wallpapers/Konachan.com - 367010 animated autumn building clouds leaves lennsan no_humans original scenic shrine sky sunset tree.gif
deleted file mode 100755
index 7ca9bd1..0000000
Binary files a/assets/wallpapers/Konachan.com - 367010 animated autumn building clouds leaves lennsan no_humans original scenic shrine sky sunset tree.gif and /dev/null differ
diff --git a/flake.lock b/flake.lock
index 038dd2c..5db808a 100644
--- a/flake.lock
+++ b/flake.lock
@@ -80,54 +80,61 @@
"type": "github"
}
},
- "crane": {
+ "determinate": {
+ "inputs": {
+ "determinate-nixd-aarch64-darwin": "determinate-nixd-aarch64-darwin",
+ "determinate-nixd-aarch64-linux": "determinate-nixd-aarch64-linux",
+ "determinate-nixd-x86_64-linux": "determinate-nixd-x86_64-linux",
+ "nix": "nix",
+ "nixpkgs": "nixpkgs_2"
+ },
"locked": {
- "lastModified": 1785782307,
- "narHash": "sha256-MPaRdVkf6zZP5fCPxYCi8Dr4pZzgmXzg8T9nVEbp3Mw=",
- "owner": "ipetkov",
- "repo": "crane",
- "rev": "2c71e194474d13de031d729b729c968ddbe3507f",
- "type": "github"
+ "lastModified": 1787168041,
+ "narHash": "sha256-KDdZe7z3BA1dYet02fDFTUplCl0gha+2ltGxMgCVA00=",
+ "rev": "6069a65ab09f683cfd9cb17b21a66a366c8077da",
+ "revCount": 432,
+ "type": "tarball",
+ "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/determinate/3.22.1/01a01b87-a478-7c30-bfa0-e436091ec74b/source.tar.gz"
},
"original": {
- "owner": "ipetkov",
- "repo": "crane",
- "type": "github"
+ "type": "tarball",
+ "url": "https://flakehub.com/f/DeterminateSystems/determinate/3"
}
},
- "crane_2": {
+ "determinate-nixd-aarch64-darwin": {
+ "flake": false,
"locked": {
- "lastModified": 1785782307,
- "narHash": "sha256-MPaRdVkf6zZP5fCPxYCi8Dr4pZzgmXzg8T9nVEbp3Mw=",
- "owner": "ipetkov",
- "repo": "crane",
- "rev": "2c71e194474d13de031d729b729c968ddbe3507f",
- "type": "github"
+ "narHash": "sha256-wMxUMoLK3e68VTDdI8JC6fZKjBux7Meh8U+3g/wnKSE=",
+ "type": "file",
+ "url": "https://install.determinate.systems/determinate-nixd/tag/v3.22.1/macOS"
},
"original": {
- "owner": "ipetkov",
- "repo": "crane",
- "type": "github"
+ "type": "file",
+ "url": "https://install.determinate.systems/determinate-nixd/tag/v3.22.1/macOS"
}
},
- "dedsec-grub-theme": {
- "inputs": {
- "nixpkgs": [
- "nixpkgs"
- ]
+ "determinate-nixd-aarch64-linux": {
+ "flake": false,
+ "locked": {
+ "narHash": "sha256-LqG2aJXwkAeemdmDvhWjADh2ZpadI32jrsFfCsZTa1Q=",
+ "type": "file",
+ "url": "https://install.determinate.systems/determinate-nixd/tag/v3.22.1/aarch64-linux"
},
+ "original": {
+ "type": "file",
+ "url": "https://install.determinate.systems/determinate-nixd/tag/v3.22.1/aarch64-linux"
+ }
+ },
+ "determinate-nixd-x86_64-linux": {
+ "flake": false,
"locked": {
- "lastModified": 1662253348,
- "narHash": "sha256-Cq/espQSpscC89oIuJGE4IP2p6zYpuzp88L6M38uayA=",
- "owner": "VandalByte",
- "repo": "dedsec-grub-theme",
- "rev": "8ccb1beb831f459987522d58263b076827624119",
- "type": "gitlab"
+ "narHash": "sha256-sXxr7bL0luNTafC9rvjh34sE1nrub0UQjhB9KmHjum0=",
+ "type": "file",
+ "url": "https://install.determinate.systems/determinate-nixd/tag/v3.22.1/x86_64-linux"
},
"original": {
- "owner": "VandalByte",
- "repo": "dedsec-grub-theme",
- "type": "gitlab"
+ "type": "file",
+ "url": "https://install.determinate.systems/determinate-nixd/tag/v3.22.1/x86_64-linux"
}
},
"disko": {
@@ -183,6 +190,27 @@
}
},
"flake-parts": {
+ "inputs": {
+ "nixpkgs-lib": [
+ "determinate",
+ "nix",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1782949081,
+ "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
+ "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
+ "revCount": 480,
+ "type": "tarball",
+ "url": "https://api.flakehub.com/f/pinned/hercules-ci/flake-parts/0.1.480%2Brev-17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e/019f2195-dee5-7233-9747-eca0c27f7406/source.tar.gz"
+ },
+ "original": {
+ "type": "tarball",
+ "url": "https://flakehub.com/f/hercules-ci/flake-parts/0.1"
+ }
+ },
+ "flake-parts_2": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
@@ -200,6 +228,28 @@
"type": "github"
}
},
+ "git-hooks-nix": {
+ "inputs": {
+ "flake-compat": "flake-compat",
+ "nixpkgs": [
+ "determinate",
+ "nix",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1784288435,
+ "narHash": "sha256-ReRHaLgr/uVqdD8afFSn+myXIfpHeOhP0yYe0TJqAA8=",
+ "rev": "43b3c1ab9d40fb1dbb008f451988a91e375825e9",
+ "revCount": 1231,
+ "type": "tarball",
+ "url": "https://api.flakehub.com/f/pinned/cachix/git-hooks.nix/0.1.1231%2Brev-43b3c1ab9d40fb1dbb008f451988a91e375825e9/019f7135-8fdf-76f0-b1a1-d2c67e91af8d/source.tar.gz"
+ },
+ "original": {
+ "type": "tarball",
+ "url": "https://flakehub.com/f/cachix/git-hooks.nix/0.1.941"
+ }
+ },
"home-manager": {
"inputs": {
"nixpkgs": [
@@ -311,7 +361,7 @@
"hyprutils": "hyprutils",
"hyprwayland-scanner": "hyprwayland-scanner",
"hyprwire": "hyprwire",
- "nixpkgs": "nixpkgs",
+ "nixpkgs": "nixpkgs_3",
"pre-commit-hooks": "pre-commit-hooks",
"systems": "systems",
"xdph": "xdph"
@@ -563,29 +613,6 @@
"type": "github"
}
},
- "lanzaboote": {
- "inputs": {
- "crane": "crane_2",
- "nixpkgs": [
- "nixpkgs"
- ],
- "pre-commit": "pre-commit",
- "rust-overlay": "rust-overlay"
- },
- "locked": {
- "lastModified": 1786958661,
- "narHash": "sha256-DAH84NNJezuk7M4WfAZtLQzJnofpXJFZFVoTPgCyUC0=",
- "owner": "nix-community",
- "repo": "lanzaboote",
- "rev": "b1b7d87faa06649f7c72666107bac2c6e6459c23",
- "type": "github"
- },
- "original": {
- "owner": "nix-community",
- "repo": "lanzaboote",
- "type": "github"
- }
- },
"m3shapes": {
"flake": false,
"locked": {
@@ -661,6 +688,27 @@
"type": "github"
}
},
+ "nix": {
+ "inputs": {
+ "flake-parts": "flake-parts",
+ "git-hooks-nix": "git-hooks-nix",
+ "nixpkgs": "nixpkgs",
+ "nixpkgs-23-11": "nixpkgs-23-11",
+ "nixpkgs-regression": "nixpkgs-regression"
+ },
+ "locked": {
+ "lastModified": 1787146895,
+ "narHash": "sha256-zKZlnOTtMHwri2kxYPziIGkLL7Q9go0CrwGbZ/7wD6s=",
+ "rev": "9dee162cae223df4d358182b4871ebad258743aa",
+ "revCount": 27234,
+ "type": "tarball",
+ "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/nix-src/3.22.1/01a01a62-3751-7452-879c-be17f74a7df2/source.tar.gz"
+ },
+ "original": {
+ "type": "tarball",
+ "url": "https://flakehub.com/f/DeterminateSystems/nix-src/%2A"
+ }
+ },
"nix-index-database": {
"inputs": {
"nixpkgs": [
@@ -683,17 +731,31 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1786247143,
- "narHash": "sha256-8S3Kcxs7D4UtxJxSJZz0m14CGhuW0MxfrIwJxeGWGnQ=",
+ "lastModified": 1784160687,
+ "narHash": "sha256-iYL/bixrb6FlHFu/gIuBYzq6c6lM5AAXsXNSWXtIgQc=",
+ "rev": "4382ed2b7a6839d4280a9b386db49cbc5907414d",
+ "revCount": 1009383,
+ "type": "tarball",
+ "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2605.1009383%2Brev-4382ed2b7a6839d4280a9b386db49cbc5907414d/019f6c11-ad78-7500-a194-d18a5bad0fbe/source.tar.gz"
+ },
+ "original": {
+ "type": "tarball",
+ "url": "https://flakehub.com/f/NixOS/nixpkgs/0.2605"
+ }
+ },
+ "nixpkgs-23-11": {
+ "locked": {
+ "lastModified": 1717159533,
+ "narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "279b4a8275f032c566576b3f181fa0f27197f588",
+ "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
"type": "github"
},
"original": {
"owner": "NixOS",
- "ref": "nixos-unstable",
"repo": "nixpkgs",
+ "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
"type": "github"
}
},
@@ -712,6 +774,22 @@
"type": "github"
}
},
+ "nixpkgs-regression": {
+ "locked": {
+ "lastModified": 1643052045,
+ "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
+ "type": "github"
+ }
+ },
"nixpkgs-stable": {
"locked": {
"lastModified": 1782847189,
@@ -730,11 +808,25 @@
},
"nixpkgs_2": {
"locked": {
- "lastModified": 1787001381,
- "narHash": "sha256-Ue1Yo8gfHdD4TMtNewhA4tkSYeFqXThju0nCyJc3ALo=",
+ "lastModified": 1786247143,
+ "narHash": "sha256-8S3Kcxs7D4UtxJxSJZz0m14CGhuW0MxfrIwJxeGWGnQ=",
+ "rev": "279b4a8275f032c566576b3f181fa0f27197f588",
+ "revCount": 1050399,
+ "type": "tarball",
+ "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/nixpkgs-weekly/0.1.1050399%2Brev-279b4a8275f032c566576b3f181fa0f27197f588/01a01364-0c36-7a91-b20d-31b3a834ddeb/source.tar.gz"
+ },
+ "original": {
+ "type": "tarball",
+ "url": "https://flakehub.com/f/DeterminateSystems/nixpkgs-weekly/0.1"
+ }
+ },
+ "nixpkgs_3": {
+ "locked": {
+ "lastModified": 1786247143,
+ "narHash": "sha256-8S3Kcxs7D4UtxJxSJZz0m14CGhuW0MxfrIwJxeGWGnQ=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "ec2d622de0773551768cf98f3fc50cbcc003b9c5",
+ "rev": "279b4a8275f032c566576b3f181fa0f27197f588",
"type": "github"
},
"original": {
@@ -744,7 +836,23 @@
"type": "github"
}
},
- "nixpkgs_3": {
+ "nixpkgs_4": {
+ "locked": {
+ "lastModified": 1787135253,
+ "narHash": "sha256-RD2kNWCG+Bjo6h+JVjWVNntZs2GtRoeY2xHjts/FNkA=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "ffb3c9b700e759be2ef13237c9d8f953b32a1e46",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs_5": {
"locked": {
"lastModified": 1786599213,
"narHash": "sha256-yjGG65gA1jORt0hWxjzrnw3AYkIn8EWolp8vezwXZ3I=",
@@ -757,7 +865,7 @@
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.zst"
}
},
- "nixpkgs_4": {
+ "nixpkgs_6": {
"locked": {
"lastModified": 1768127708,
"narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=",
@@ -773,7 +881,7 @@
"type": "github"
}
},
- "nixpkgs_5": {
+ "nixpkgs_7": {
"locked": {
"lastModified": 1786599213,
"narHash": "sha256-yNJd40f11EzXBjSByCB7IPpeFFAdeoSKKM67dGkfFoU=",
@@ -791,7 +899,7 @@
},
"noctalia": {
"inputs": {
- "nixpkgs": "nixpkgs_3"
+ "nixpkgs": "nixpkgs_5"
},
"locked": {
"lastModified": 1787021519,
@@ -807,31 +915,9 @@
"type": "github"
}
},
- "pre-commit": {
- "inputs": {
- "flake-compat": "flake-compat_2",
- "nixpkgs": [
- "lanzaboote",
- "nixpkgs"
- ]
- },
- "locked": {
- "lastModified": 1784288435,
- "narHash": "sha256-ReRHaLgr/uVqdD8afFSn+myXIfpHeOhP0yYe0TJqAA8=",
- "owner": "cachix",
- "repo": "git-hooks.nix",
- "rev": "43b3c1ab9d40fb1dbb008f451988a91e375825e9",
- "type": "github"
- },
- "original": {
- "owner": "cachix",
- "repo": "git-hooks.nix",
- "type": "github"
- }
- },
"pre-commit-hooks": {
"inputs": {
- "flake-compat": "flake-compat",
+ "flake-compat": "flake-compat_2",
"nixpkgs": [
"hyprland",
"nixpkgs"
@@ -875,43 +961,20 @@
"root": {
"inputs": {
"caelestia-shell": "caelestia-shell",
- "crane": "crane",
- "dedsec-grub-theme": "dedsec-grub-theme",
+ "determinate": "determinate",
"disko": "disko",
- "flake-parts": "flake-parts",
+ "flake-parts": "flake-parts_2",
"home-manager": "home-manager",
"hyprland": "hyprland",
- "lanzaboote": "lanzaboote",
"niri": "niri",
"nix-index-database": "nix-index-database",
- "nixpkgs": "nixpkgs_2",
+ "nixpkgs": "nixpkgs_4",
"noctalia": "noctalia",
"sops-nix": "sops-nix",
"tmux-powerkit": "tmux-powerkit",
"zen-browser": "zen-browser"
}
},
- "rust-overlay": {
- "inputs": {
- "nixpkgs": [
- "lanzaboote",
- "nixpkgs"
- ]
- },
- "locked": {
- "lastModified": 1786849542,
- "narHash": "sha256-MS8cOa/ii1+QA8R3JWVzqEIoXimu9n50GwQDiBVTFOM=",
- "owner": "oxalica",
- "repo": "rust-overlay",
- "rev": "b211eadeba8b180da9453ec3413a8a3535c85b3f",
- "type": "github"
- },
- "original": {
- "owner": "oxalica",
- "repo": "rust-overlay",
- "type": "github"
- }
- },
"sops-nix": {
"inputs": {
"nixpkgs": [
@@ -949,7 +1012,7 @@
},
"tmux-powerkit": {
"inputs": {
- "nixpkgs": "nixpkgs_4"
+ "nixpkgs": "nixpkgs_6"
},
"locked": {
"lastModified": 1786964392,
@@ -1042,7 +1105,7 @@
"zen-browser": {
"inputs": {
"home-manager": "home-manager_2",
- "nixpkgs": "nixpkgs_5"
+ "nixpkgs": "nixpkgs_7"
},
"locked": {
"lastModified": 1787021741,
diff --git a/flake.nix b/flake.nix
index 6f66772..4900909 100644
--- a/flake.nix
+++ b/flake.nix
@@ -6,52 +6,59 @@
"https://cache.nixos.org/"
"https://noctalia.cachix.org"
"https://hyprland.cachix.org"
+ "https://install.determinate.systems"
];
extra-trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"noctalia.cachix.org-1:pCOR47nnMEo5thcxNDtzWpOxNFQsBRglJzxWPp3dkU4="
"hyprland.cachix.org-1:a7pgxzMz7+chwVL3/pzj6jIBMioiJM7ypFP8PwtkuGc="
+ "cache.flakehub.com-3:hJuILl5sVK4iKm86JzgdXW12Y2Hwd5G07qKtHTOcDCM="
];
};
inputs = {
+ # ── Core & Flake Infrastructure ─────────────────────────────────────
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
- zen-browser.url = "github:0xc000022070/zen-browser-flake";
- tmux-powerkit.url = "github:fabioluciano/tmux-powerkit";
- home-manager.url = "github:nix-community/home-manager/master";
- home-manager.inputs.nixpkgs.follows = "nixpkgs";
- hyprland.url = "git+https://github.com/hyprwm/Hyprland?submodules=1";
- niri.url = "github:sodiboo/niri-flake";
- niri.inputs.nixpkgs.follows = "nixpkgs";
- noctalia.url = "github:noctalia-dev/noctalia-shell";
- caelestia-shell.url = "github:caelestia-dots/shell";
- caelestia-shell.inputs.nixpkgs.follows = "nixpkgs";
- nix-index-database.url = "github:nix-community/nix-index-database";
- nix-index-database.inputs.nixpkgs.follows = "nixpkgs";
- disko.url = "github:nix-community/disko";
- disko.inputs.nixpkgs.follows = "nixpkgs";
- crane.url = "github:ipetkov/crane";
- dedsec-grub-theme.url = "gitlab:VandalByte/dedsec-grub-theme";
- dedsec-grub-theme.inputs.nixpkgs.follows = "nixpkgs";
- lanzaboote = {
- url = "github:nix-community/lanzaboote";
+
+ # ── System Utilities & Nix Distribution ─────────────────────────────
+ determinate.url = "https://flakehub.com/f/DeterminateSystems/determinate/3";
+ disko = {
+ url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
+ nix-index-database = {
+ url = "github:nix-community/nix-index-database";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+
+ # ── Desktop & Wayland Ecosystem ─────────────────────────────────────
+ home-manager = {
+ url = "github:nix-community/home-manager/master";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ hyprland.url = "git+https://github.com/hyprwm/Hyprland?submodules=1";
+ niri = {
+ url = "github:sodiboo/niri-flake";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ noctalia.url = "github:noctalia-dev/noctalia-shell";
+ caelestia-shell = {
+ url = "github:caelestia-dots/shell";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ zen-browser.url = "github:0xc000022070/zen-browser-flake";
+ tmux-powerkit.url = "github:fabioluciano/tmux-powerkit";
};
outputs =
inputs:
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" ];
- imports = [
- ./flake/hosts.nix
- ./flake/installer.nix
- ./flake/rust-installer.nix
- ];
+ imports = [ ./flake/modules.nix ];
};
}
diff --git a/flake/checks.nix b/flake/checks.nix
new file mode 100644
index 0000000..862c27e
--- /dev/null
+++ b/flake/checks.nix
@@ -0,0 +1,82 @@
+# Flake-parts module: Nix-native checks via `nix flake check`
+{ self, inputs, ... }:
+
+{
+ perSystem =
+ { pkgs, system, ... }:
+ let
+ lib = inputs.nixpkgs.lib;
+ northstar = import ../lib/core.nix { inherit lib; };
+
+ in
+ {
+ checks = {
+
+ # Verify lib functions behave correctly
+ lib-unit-tests =
+ pkgs.runCommand "check-lib-functions"
+ {
+ nativeBuildInputs = [
+ pkgs.nix
+ pkgs.jq
+ ];
+ NIX_CONF_DIR = pkgs.writeTextDir "nix.conf" ''
+ experimental-features = nix-command flakes pipe-operators
+ '';
+ }
+ ''
+ export NIX_STATE_DIR=$TMPDIR/nix
+ export NIX_CACHE_DIR=$TMPDIR/nix-cache
+ mkdir -p $NIX_STATE_DIR $NIX_CACHE_DIR
+
+ echo "=== Testing lib functions ==="
+
+ # Test: scanModules finds modules
+ MODULE_COUNT=$(${pkgs.nix}/bin/nix eval --impure --expr '
+ let
+ lib = import ${inputs.nixpkgs} { system = "${system}"; };
+ northstar = import ${../lib/core.nix} { inherit (lib) lib; };
+ in builtins.length (northstar.scanModules ${../modules})
+ ')
+ echo "scanModules found $MODULE_COUNT modules"
+ if [ "$MODULE_COUNT" -lt 1 ]; then
+ echo "FAIL: scanModules returned 0 modules"
+ exit 1
+ fi
+
+ # Test: discoverHosts finds hosts
+ HOSTS=$(${pkgs.nix}/bin/nix eval --impure --json --expr '
+ let
+ lib = import ${inputs.nixpkgs} { system = "${system}"; };
+ northstar = import ${../lib/core.nix} { inherit (lib) lib; };
+ in northstar.discoverHosts ${../hosts}
+ ')
+ echo "discoverHosts found: $HOSTS"
+ echo "$HOSTS" | jq -e 'length > 0' > /dev/null || {
+ echo "FAIL: discoverHosts returned 0 hosts"
+ exit 1
+ }
+
+ # Test: mkProfile produces correct attrs
+ PROFILE_KEYS=$(${pkgs.nix}/bin/nix eval --impure --json --expr '
+ let
+ lib = import ${inputs.nixpkgs} { system = "${system}"; };
+ northstar = import ${../lib/core.nix} { inherit (lib) lib; };
+ profile = northstar.mkProfile ["boot" "ssh"];
+ in builtins.attrNames profile.northstar.features
+ ')
+ echo "mkProfile keys: $PROFILE_KEYS"
+ echo "$PROFILE_KEYS" | jq -e 'index("boot") and index("ssh")' > /dev/null || {
+ echo "FAIL: mkProfile did not produce boot and ssh keys"
+ exit 1
+ }
+
+ echo "=== All lib tests passed ==="
+ touch $out
+ '';
+
+ # Verify the installer package builds
+ installer-builds = self.packages.${system}.installer;
+ };
+ };
+}
diff --git a/flake/devshell.nix b/flake/devshell.nix
new file mode 100644
index 0000000..7327f0c
--- /dev/null
+++ b/flake/devshell.nix
@@ -0,0 +1,35 @@
+# Flake-parts module: development shell for contributors
+{ inputs, ... }:
+
+{
+ perSystem =
+ { pkgs, system, ... }:
+ {
+ devShells.default = pkgs.mkShell {
+ name = "northstar-dev";
+ packages = with pkgs; [
+ # Nix tooling
+ inputs.determinate.packages.${system}.default
+ nixfmt
+ nil
+ nix-diff
+
+ # Python (for installer tests)
+ python3
+
+ # General
+ git
+ jq
+ ];
+
+ shellHook = ''
+ echo "❄️ Northstar development shell"
+ echo ""
+ echo " nix flake check --impure — run all checks"
+ echo " nix fmt — format all Nix files"
+ echo " python3 -m unittest discover -s tests -v — run Python tests"
+ echo ""
+ '';
+ };
+ };
+}
diff --git a/flake/formatter.nix b/flake/formatter.nix
new file mode 100644
index 0000000..b16ea6a
--- /dev/null
+++ b/flake/formatter.nix
@@ -0,0 +1,10 @@
+# Flake-parts module: code formatter
+{ inputs, ... }:
+
+{
+ perSystem =
+ { pkgs, ... }:
+ {
+ formatter = pkgs.nixfmt;
+ };
+}
diff --git a/flake/hosts.nix b/flake/hosts.nix
index bdc7afa..db9582a 100644
--- a/flake/hosts.nix
+++ b/flake/hosts.nix
@@ -3,10 +3,10 @@
let
lib = inputs.nixpkgs.lib;
- northstar = import ../lib { inherit lib; };
- modulePaths = ../modules |> northstar.scanModules;
+ northstar = import ../lib/core.nix { inherit lib; };
+ modulePaths = northstar.scanModules ../modules;
hostsDir = ../hosts;
- hosts = hostsDir |> northstar.discoverHosts;
+ hosts = northstar.discoverHosts hostsDir;
mkHost =
hostName:
@@ -28,10 +28,10 @@ let
in
{
flake = {
- nixosConfigurations = hosts |> (h: lib.genAttrs h mkHost);
+ nixosConfigurations = lib.genAttrs hosts mkHost;
nixosModules.default = {
imports = modulePaths;
};
- homeManagerModules.default = import ../home;
+ homeManagerModules.default = import ../home/home.nix;
};
}
diff --git a/flake/installer.nix b/flake/installer.nix
index a293d2c..1b0f104 100644
--- a/flake/installer.nix
+++ b/flake/installer.nix
@@ -12,6 +12,7 @@
installer = pkgs.writeShellApplication {
name = "northstar-install";
runtimeInputs = with pkgs; [
+ inputs.determinate.packages.${system}.default
nixos-install-tools
age
ssh-to-age
diff --git a/flake/modules.nix b/flake/modules.nix
new file mode 100644
index 0000000..67ebd2a
--- /dev/null
+++ b/flake/modules.nix
@@ -0,0 +1,10 @@
+# Flake-parts module: single aggregator entrypoint for all flake modules
+{
+ imports = [
+ ./hosts.nix
+ ./installer.nix
+ ./checks.nix
+ ./devshell.nix
+ ./formatter.nix
+ ];
+}
diff --git a/flake/rust-installer.nix b/flake/rust-installer.nix
deleted file mode 100644
index 590bcb6..0000000
--- a/flake/rust-installer.nix
+++ /dev/null
@@ -1,103 +0,0 @@
-# Flake-parts module: Rust binary installer (crane)
-{ self, inputs, ... }:
-
-{
- perSystem =
- { pkgs, system, ... }:
- let
- craneLib = inputs.crane.mkLib pkgs;
-
- # Use the installer-rs directory directly so crane can read Cargo.toml
- # at eval time. We'll populate the flake/ dir in the build phase.
- rustSrc = pkgs.lib.cleanSourceWith {
- src = ../installer-rs;
- filter =
- path: type:
- (craneLib.filterCargoSources path type)
- || (type == "directory" && builtins.baseNameOf path == "flake")
- || builtins.baseNameOf path == "PLACEHOLDER";
- };
-
- # Assemble the full flake source as a derivation
- flakeSrc = pkgs.runCommand "northstar-flake-src" { } ''
- mkdir -p $out
- cp ${../flake.nix} $out/flake.nix
- cp ${../flake.lock} $out/flake.lock
- cp ${../README.md} $out/README.md
- cp -r ${../hosts} $out/hosts
- cp -r ${../home} $out/home
- cp -r ${../lib} $out/lib
- cp -r ${../modules} $out/modules
- cp -r ${../flake} $out/flake
- cp -r ${../assets} $out/assets
- '';
-
- commonArgs = {
- src = rustSrc;
- pname = "northstar-installer";
- version = "2.0.0";
- strictDeps = true;
- nativeBuildInputs = [ pkgs.pkg-config ];
-
- # Populate flake/ dir before cargo build so include_dir! works
- preBuild = ''
- mkdir -p flake
- rm -rf flake/*
- cp -r ${flakeSrc}/* flake/
- chmod -R u+w flake
- '';
- };
-
- cargoArtifacts = craneLib.buildDepsOnly commonArgs;
-
- rustBin = craneLib.buildPackage (commonArgs // { inherit cargoArtifacts; });
-
- # Runtime tools the installer shells out to
- runtimeDeps = with pkgs; [
- git
- coreutils
- util-linux
- pciutils
- whois
- openssl
- parted
- btrfs-progs
- e2fsprogs
- nixos-install-tools
- ];
-
- # Deprecation wrapper: prints prominent notice and invokes the primary python installer
- deprecatedRustInstall = pkgs.writeShellApplication {
- name = "northstar-rust-install-deprecated";
- runtimeInputs = [ self.packages.${system}.installer ];
- text = ''
- echo "================================================================================"
- echo " [WARNING] The Rust-based installer ('installer-rs' / .#rust-install) is DEPRECATED."
- echo " Redirecting to the primary Northstar Python installer (northstar-install)..."
- echo "================================================================================"
- exec northstar-install "$@"
- '';
- };
- in
- {
- packages.rust-installer = pkgs.symlinkJoin {
- name = "northstar-installer";
- paths = [ rustBin ];
- nativeBuildInputs = [ pkgs.makeWrapper ];
- postBuild = ''
- wrapProgram $out/bin/northstar-installer \
- --prefix PATH : ${pkgs.lib.makeBinPath runtimeDeps}
- '';
- meta = {
- description = "Northstar NixOS installer (Rust + ratatui)";
- mainProgram = "northstar-installer";
- };
- };
-
- apps.rust-install = {
- type = "app";
- program = "${deprecatedRustInstall}/bin/northstar-rust-install-deprecated";
- meta.description = "Rust-based Northstar installer with ratatui TUI (DEPRECATED - redirects to Python installer)";
- };
- };
-}
diff --git a/home/default.nix b/home/home.nix
similarity index 100%
rename from home/default.nix
rename to home/home.nix
diff --git a/hosts/Makima/default.nix b/hosts/Makima/default.nix
index 48694ec..9ae72f6 100644
--- a/hosts/Makima/default.nix
+++ b/hosts/Makima/default.nix
@@ -7,21 +7,19 @@
{
imports = [
+ ./hardware.nix
./disko.nix
];
home-manager.users.reze = {
- imports = [ ../../home ];
+ imports = [ ../../home/home.nix ];
home.username = lib.mkForce "reze";
home.homeDirectory = lib.mkForce "/home/reze";
};
- boot.loader.grub.extraEntries = ''
- menuentry "Fedora" {
- search --fs-uuid --set=root CB41-6695
- chainloader /EFI/fedora/shimx64.efi
- }
- '';
+ # Bootloader (Limine)
+ boot.loader.limine.resolution = "1920x1080";
+ northstar.features.boot.secureBoot.enable = true;
users.users.reze = {
isNormalUser = true;
@@ -33,15 +31,22 @@
"docker"
];
shell = pkgs.zsh;
- hashedPassword = "$6$oXzrheHsEVSMJyV3$KzMdb2T8CHzGwcAlJqE3khEVfH/b5jFs/n5vNriwifcJ9mlgbsB221oILkizsjSYcFVJ5/kkYfjt8M2QFkkNl0";
+ hashedPassword = "$6$7uVH9VA23imtOFPs$Rx7oc7xoN5gxBqdB6pg1ZG7xqAeX4LIzLuKjPExFOySTdfmVGdDbCD.4K/dtLLbUbdpcNJ8W5OYpeknaij6mM.";
};
- #Northstar profiles
+ # Northstar profiles
northstar.profiles = {
desktop.enable = true;
workstation.enable = true;
};
+ # Custom feature overrides
+ northstar.features = {
+ niri.enable = true;
+ fish.enable = true;
+ emacs.enable = true;
+ };
+
# NVIDIA GPU
northstar.nvidia.enable = true;
northstar.nvidia.prime = {
diff --git a/hosts/Makima/disko.nix b/hosts/Makima/disko.nix
index b56d8ce..9f44bba 100644
--- a/hosts/Makima/disko.nix
+++ b/hosts/Makima/disko.nix
@@ -1,36 +1,14 @@
-# Auto-generated disko config for Makima (partition-only)
-{
- disko.devices.disk.nixos = {
- type = "disk";
- device = "/dev/nvme0n1p4";
- content = {
- type = "btrfs";
- extraArgs = [ "-f" ];
- subvolumes = {
- "/root" = {
- mountpoint = "/";
- mountOptions = [ "compress=zstd" ];
- };
- "/home" = {
- mountpoint = "/home";
- mountOptions = [ "compress=zstd" ];
- };
- "/nix" = {
- mountpoint = "/nix";
- mountOptions = [ "compress=zstd" "noatime" ];
- };
- "/log" = {
- mountpoint = "/var/log";
- mountOptions = [ "compress=zstd" ];
- };
- };
- };
- };
+# Auto-generated disko config for Makima
+{ lib, ... }:
- # Existing EFI partition — not managed by disko
- fileSystems."/boot/efi" = {
- device = "/dev/disk/by-uuid/CB41-6695";
- fsType = "vfat";
- options = [ "fmask=0022" "dmask=0022" ];
- };
+let
+ northstar = import ../../lib/core.nix { inherit lib; };
+in
+northstar.mkDisko {
+ mode = "whole-disk";
+ device = "/dev/nvme0n1";
+ fsType = "btrfs";
+ efiSize = "4G";
+ swapSize = "16G";
+ rootSize = "270G";
}
diff --git a/hosts/Makima/hardware.nix b/hosts/Makima/hardware.nix
index e2ef9a0..3ce958a 100644
--- a/hosts/Makima/hardware.nix
+++ b/hosts/Makima/hardware.nix
@@ -1,18 +1,32 @@
-# Do not modify this file! It was generated by ‘nixos-generate-config’
-# and may be overwritten by future invocations. Please make changes
-# to /etc/nixos/configuration.nix instead.
-{ config, lib, pkgs, modulesPath, ... }:
+{
+ config,
+ lib,
+ pkgs,
+ modulesPath,
+ ...
+}:
{
- imports =
- [ (modulesPath + "/installer/scan/not-detected.nix")
- ];
+ imports = [
+ (modulesPath + "/installer/scan/not-detected.nix")
+ ];
- boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usbhid" "usb_storage" "sd_mod" ];
+ boot.initrd.availableKernelModules = [
+ "nvme"
+ "xhci_pci"
+ "ahci"
+ "usbhid"
+ "usb_storage"
+ "sd_mod"
+ ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-amd" ];
boot.extraModulePackages = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
+ hardware = {
+ graphics.enable = lib.mkDefault true;
+ firmware = [ pkgs.linux-firmware ];
+ };
}
diff --git a/hosts/common.nix b/hosts/common.nix
index 35164e5..157cb2b 100644
--- a/hosts/common.nix
+++ b/hosts/common.nix
@@ -2,6 +2,7 @@
{
imports = [
+ inputs.determinate.nixosModules.default
inputs.home-manager.nixosModules.home-manager
inputs.nix-index-database.nixosModules.nix-index
];
diff --git a/installer-rs/Cargo.lock b/installer-rs/Cargo.lock
deleted file mode 100644
index f6942b8..0000000
--- a/installer-rs/Cargo.lock
+++ /dev/null
@@ -1,755 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "allocator-api2"
-version = "0.2.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
-
-[[package]]
-name = "anyhow"
-version = "1.0.103"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
-
-[[package]]
-name = "bitflags"
-version = "2.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
-
-[[package]]
-name = "bytes"
-version = "1.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
-
-[[package]]
-name = "cassowary"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
-
-[[package]]
-name = "castaway"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
-dependencies = [
- "rustversion",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "cfg_aliases"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
-
-[[package]]
-name = "compact_str"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e"
-dependencies = [
- "castaway",
- "cfg-if",
- "itoa",
- "rustversion",
- "ryu",
- "static_assertions",
-]
-
-[[package]]
-name = "crossterm"
-version = "0.28.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
-dependencies = [
- "bitflags",
- "crossterm_winapi",
- "mio",
- "parking_lot",
- "rustix",
- "signal-hook",
- "signal-hook-mio",
- "winapi",
-]
-
-[[package]]
-name = "crossterm_winapi"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
-dependencies = [
- "winapi",
-]
-
-[[package]]
-name = "darling"
-version = "0.23.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
-dependencies = [
- "darling_core",
- "darling_macro",
-]
-
-[[package]]
-name = "darling_core"
-version = "0.23.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
-dependencies = [
- "ident_case",
- "proc-macro2",
- "quote",
- "strsim",
- "syn",
-]
-
-[[package]]
-name = "darling_macro"
-version = "0.23.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
-dependencies = [
- "darling_core",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "either"
-version = "1.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
-
-[[package]]
-name = "equivalent"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
-
-[[package]]
-name = "errno"
-version = "0.3.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
-dependencies = [
- "libc",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "foldhash"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
-
-[[package]]
-name = "hashbrown"
-version = "0.15.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
-dependencies = [
- "allocator-api2",
- "equivalent",
- "foldhash",
-]
-
-[[package]]
-name = "heck"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
-
-[[package]]
-name = "ident_case"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
-
-[[package]]
-name = "include_dir"
-version = "0.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
-dependencies = [
- "include_dir_macros",
-]
-
-[[package]]
-name = "include_dir_macros"
-version = "0.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
-dependencies = [
- "proc-macro2",
- "quote",
-]
-
-[[package]]
-name = "indoc"
-version = "2.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
-dependencies = [
- "rustversion",
-]
-
-[[package]]
-name = "instability"
-version = "0.3.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
-dependencies = [
- "darling",
- "indoc",
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "itertools"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
-dependencies = [
- "either",
-]
-
-[[package]]
-name = "itoa"
-version = "1.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
-
-[[package]]
-name = "libc"
-version = "0.2.186"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
-
-[[package]]
-name = "linux-raw-sys"
-version = "0.4.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
-
-[[package]]
-name = "lock_api"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
-dependencies = [
- "scopeguard",
-]
-
-[[package]]
-name = "log"
-version = "0.4.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
-
-[[package]]
-name = "lru"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
-dependencies = [
- "hashbrown",
-]
-
-[[package]]
-name = "memchr"
-version = "2.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
-
-[[package]]
-name = "mio"
-version = "1.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
-dependencies = [
- "libc",
- "log",
- "wasi",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "nix"
-version = "0.30.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
-dependencies = [
- "bitflags",
- "cfg-if",
- "cfg_aliases",
- "libc",
-]
-
-[[package]]
-name = "northstar-installer"
-version = "2.1.0"
-dependencies = [
- "anyhow",
- "crossterm",
- "include_dir",
- "nix",
- "ratatui",
- "serde",
- "serde_json",
- "tokio",
- "zeroize",
-]
-
-[[package]]
-name = "parking_lot"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-link",
-]
-
-[[package]]
-name = "paste"
-version = "1.0.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
-
-[[package]]
-name = "pin-project-lite"
-version = "0.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.106"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.46"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "ratatui"
-version = "0.29.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
-dependencies = [
- "bitflags",
- "cassowary",
- "compact_str",
- "crossterm",
- "indoc",
- "instability",
- "itertools",
- "lru",
- "paste",
- "strum",
- "unicode-segmentation",
- "unicode-truncate",
- "unicode-width 0.2.0",
-]
-
-[[package]]
-name = "redox_syscall"
-version = "0.5.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
-dependencies = [
- "bitflags",
-]
-
-[[package]]
-name = "rustix"
-version = "0.38.44"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
-dependencies = [
- "bitflags",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "rustversion"
-version = "1.0.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
-
-[[package]]
-name = "ryu"
-version = "1.0.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
-
-[[package]]
-name = "scopeguard"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-
-[[package]]
-name = "serde"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
-dependencies = [
- "serde_core",
- "serde_derive",
-]
-
-[[package]]
-name = "serde_core"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
-dependencies = [
- "serde_derive",
-]
-
-[[package]]
-name = "serde_derive"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "serde_json"
-version = "1.0.150"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
-dependencies = [
- "itoa",
- "memchr",
- "serde",
- "serde_core",
- "zmij",
-]
-
-[[package]]
-name = "signal-hook"
-version = "0.3.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
-dependencies = [
- "libc",
- "signal-hook-registry",
-]
-
-[[package]]
-name = "signal-hook-mio"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
-dependencies = [
- "libc",
- "mio",
- "signal-hook",
-]
-
-[[package]]
-name = "signal-hook-registry"
-version = "1.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
-dependencies = [
- "errno",
- "libc",
-]
-
-[[package]]
-name = "smallvec"
-version = "1.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
-
-[[package]]
-name = "socket2"
-version = "0.6.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
-dependencies = [
- "libc",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "static_assertions"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
-
-[[package]]
-name = "strsim"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
-
-[[package]]
-name = "strum"
-version = "0.26.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
-dependencies = [
- "strum_macros",
-]
-
-[[package]]
-name = "strum_macros"
-version = "0.26.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
-dependencies = [
- "heck",
- "proc-macro2",
- "quote",
- "rustversion",
- "syn",
-]
-
-[[package]]
-name = "syn"
-version = "2.0.119"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "tokio"
-version = "1.53.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee"
-dependencies = [
- "bytes",
- "libc",
- "mio",
- "parking_lot",
- "pin-project-lite",
- "signal-hook-registry",
- "socket2",
- "tokio-macros",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "tokio-macros"
-version = "2.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
-
-[[package]]
-name = "unicode-segmentation"
-version = "1.13.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
-
-[[package]]
-name = "unicode-truncate"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
-dependencies = [
- "itertools",
- "unicode-segmentation",
- "unicode-width 0.1.14",
-]
-
-[[package]]
-name = "unicode-width"
-version = "0.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
-
-[[package]]
-name = "unicode-width"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
-
-[[package]]
-name = "wasi"
-version = "0.11.1+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
-
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
-[[package]]
-name = "windows-link"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
-
-[[package]]
-name = "windows-sys"
-version = "0.59.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.61.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
-dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
-]
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
-
-[[package]]
-name = "zeroize"
-version = "1.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
-
-[[package]]
-name = "zmij"
-version = "1.0.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/installer-rs/Cargo.toml b/installer-rs/Cargo.toml
deleted file mode 100644
index 457ccab..0000000
--- a/installer-rs/Cargo.toml
+++ /dev/null
@@ -1,32 +0,0 @@
-[package]
-name = "northstar-installer"
-version = "2.1.0"
-edition = "2021"
-description = "TUI-based NixOS installer for the Northstar configuration"
-authors = ["reze-dev"]
-repository = "https://github.com/reze-dev/northstar"
-license = "MIT"
-
-[lib]
-name = "northstar_installer"
-path = "src/lib.rs"
-
-[[bin]]
-name = "northstar-installer"
-path = "src/main.rs"
-
-[dependencies]
-ratatui = "0.29"
-crossterm = "0.28"
-tokio = { version = "1", features = ["full"] }
-include_dir = "0.7"
-serde = { version = "1", features = ["derive"] }
-serde_json = "1"
-nix = { version = "0.30", features = ["user"] }
-anyhow = "1"
-zeroize = "1"
-
-[profile.release]
-strip = true
-lto = true
-opt-level = "z"
diff --git a/installer-rs/flake/PLACEHOLDER b/installer-rs/flake/PLACEHOLDER
deleted file mode 100644
index 556afbc..0000000
--- a/installer-rs/flake/PLACEHOLDER
+++ /dev/null
@@ -1 +0,0 @@
-Populated at build time with the full Northstar flake. See flake/rust-installer.nix.
diff --git a/installer-rs/src/app.rs b/installer-rs/src/app.rs
deleted file mode 100644
index 82b5ffb..0000000
--- a/installer-rs/src/app.rs
+++ /dev/null
@@ -1,708 +0,0 @@
-use std::collections::VecDeque;
-use tokio::sync::mpsc;
-use tokio::task::JoinHandle;
-use zeroize::Zeroize;
-
-/// Installation mode.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub enum InstallMode {
- #[default]
- WholeDisk,
- PartitionOnly,
-}
-
-impl std::fmt::Display for InstallMode {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::WholeDisk => write!(f, "whole-disk"),
- Self::PartitionOnly => write!(f, "partition-only"),
- }
- }
-}
-
-/// Profile bundle choice.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub enum ProfileChoice {
- Base,
- #[default]
- Desktop,
- Workstation,
-}
-
-impl std::fmt::Display for ProfileChoice {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Base => write!(f, "Base (Minimal CLI Server)"),
- Self::Desktop => write!(f, "Desktop (GUI + Compositors + Browsers)"),
- Self::Workstation => write!(f, "Workstation (Desktop + Devtools + Virt)"),
- }
- }
-}
-
-/// Bootloader choice.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub enum BootloaderChoice {
- #[default]
- Grub,
- Limine,
-}
-
-impl std::fmt::Display for BootloaderChoice {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Grub => write!(f, "GRUB (Cyberpunk DedSec Theme)"),
- Self::Limine => write!(f, "Limine (Modern Ultra-Fast UEFI)"),
- }
- }
-}
-
-/// GPU driver choice.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub enum GpuChoice {
- #[default]
- None,
- Nvidia,
- NvidiaPrime,
-}
-
-impl std::fmt::Display for GpuChoice {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::None => write!(f, "Default (no NVIDIA)"),
- Self::Nvidia => write!(f, "NVIDIA Discrete"),
- Self::NvidiaPrime => write!(f, "NVIDIA Prime (Hybrid GPU)"),
- }
- }
-}
-
-/// Integrated GPU type for Prime setups.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub enum IgpuType {
- #[default]
- Intel,
- Amd,
-}
-
-impl std::fmt::Display for IgpuType {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Intel => write!(f, "intel"),
- Self::Amd => write!(f, "amd"),
- }
- }
-}
-
-impl IgpuType {
- pub fn bus_id_key(&self) -> &'static str {
- match self {
- Self::Intel => "intelBusId",
- Self::Amd => "amdgpuBusId",
- }
- }
-}
-
-/// Custom toggleable feature in the customization page.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct FeatureOption {
- pub id: String,
- pub label: String,
- pub category: String,
- pub enabled: bool,
-}
-
-/// Dual-boot OS entry detected on ESP.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct DualBootEntry {
- pub name: String,
- pub efi_path: String,
- pub disk_uuid: String,
- pub enabled: bool,
-}
-
-/// Disk information parsed from lsblk.
-#[derive(Debug, Clone, PartialEq, Eq, Default)]
-pub struct DiskInfo {
- pub name: String,
- pub size: String,
- pub model: String,
- pub drive_type: String,
- pub partitions: Vec,
-}
-
-/// Partition information parsed from lsblk.
-#[derive(Debug, Clone, PartialEq, Eq, Default)]
-pub struct PartitionInfo {
- pub name: String,
- pub size: String,
- pub fs_type: String,
- pub mountpoint: Option,
- pub label: Option,
- pub uuid: Option,
-}
-
-/// Holds all user-collected configuration for installation.
-#[derive(Debug, Clone)]
-pub struct InstallConfig {
- pub hostname: String,
- pub username: String,
- pub hashed_pw: String,
- pub profile: ProfileChoice,
- pub shell: String,
- pub bootloader: BootloaderChoice,
- pub features: Vec,
- pub dual_boot_entries: Vec,
- pub mode: InstallMode,
- pub disk_dev: String,
- pub nixos_part: String,
- pub efi_part: String,
- pub swap_size: String,
- pub swap_partition: String,
- pub fs_type: String,
- pub root_size: String,
- pub gpu_choice: GpuChoice,
- pub nvidia_bus_id: String,
- pub igpu_bus_id: String,
- pub igpu_type: IgpuType,
-}
-
-impl Default for InstallConfig {
- fn default() -> Self {
- Self {
- hostname: String::new(),
- username: String::new(),
- hashed_pw: String::new(),
- profile: ProfileChoice::Desktop,
- shell: "zsh".to_string(),
- bootloader: BootloaderChoice::Grub,
- features: default_features(ProfileChoice::Desktop),
- dual_boot_entries: Vec::new(),
- mode: InstallMode::WholeDisk,
- disk_dev: String::new(),
- nixos_part: String::new(),
- efi_part: String::new(),
- swap_size: "8G".to_string(),
- swap_partition: String::new(),
- fs_type: "btrfs".to_string(),
- root_size: "100%".to_string(),
- gpu_choice: GpuChoice::None,
- nvidia_bus_id: String::new(),
- igpu_bus_id: String::new(),
- igpu_type: IgpuType::Intel,
- }
- }
-}
-
-pub fn default_features(profile: ProfileChoice) -> Vec {
- let is_desktop = profile == ProfileChoice::Desktop || profile == ProfileChoice::Workstation;
- let is_workstation = profile == ProfileChoice::Workstation;
-
- vec![
- // Window Managers / Compositors
- FeatureOption {
- id: "hyprland".into(),
- label: "Hyprland (Dynamic Wayland Tiling WM)".into(),
- category: "Desktop / Compositor".into(),
- enabled: is_desktop,
- },
- FeatureOption {
- id: "niri".into(),
- label: "Niri (Scrollable-tiling Wayland WM)".into(),
- category: "Desktop / Compositor".into(),
- enabled: false,
- },
- FeatureOption {
- id: "noctalia".into(),
- label: "Noctalia (Custom Desktop Environment)".into(),
- category: "Desktop / Compositor".into(),
- enabled: is_desktop,
- },
- // Shells
- FeatureOption {
- id: "zsh".into(),
- label: "Zsh + Starship / OMP Shell".into(),
- category: "Shell & Terminal".into(),
- enabled: true,
- },
- FeatureOption {
- id: "fish".into(),
- label: "Fish Friendly Interactive Shell".into(),
- category: "Shell & Terminal".into(),
- enabled: false,
- },
- FeatureOption {
- id: "ghostty".into(),
- label: "Ghostty Modern Terminal".into(),
- category: "Shell & Terminal".into(),
- enabled: is_desktop,
- },
- FeatureOption {
- id: "kitty".into(),
- label: "Kitty GPU-accelerated Terminal".into(),
- category: "Shell & Terminal".into(),
- enabled: is_desktop,
- },
- // Development & Virt
- FeatureOption {
- id: "devtools".into(),
- label: "Developer Workspace (LSPs, Compilers, Tools)".into(),
- category: "Development & Virt".into(),
- enabled: is_workstation,
- },
- FeatureOption {
- id: "virtualization".into(),
- label: "Docker & Libvirt Virtualization".into(),
- category: "Development & Virt".into(),
- enabled: is_workstation,
- },
- FeatureOption {
- id: "emacs".into(),
- label: "Emacs with Doom/Custom Config".into(),
- category: "Development & Virt".into(),
- enabled: false,
- },
- ]
-}
-
-/// Progress updates sent from backend to TUI.
-#[derive(Debug, Clone)]
-pub struct ProgressUpdate {
- pub step: String,
- pub message: String,
- pub done: bool,
- pub error: Option,
-}
-
-/// Page in the wizard.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum Page {
- Welcome,
- Hostname,
- Username,
- Password,
- PasswordConfirm,
- Profile,
- ProfileCustomize,
- Bootloader,
- Mode,
- Disk,
- DiskConfirm,
- PartSelect,
- PartNewStart,
- PartNewEnd,
- PartExist,
- PartConfirm,
- Efi,
- Fs,
- RootSize,
- Swap,
- SwapPartition,
- Gpu,
- GpuNvBus,
- GpuIgpuType,
- GpuIgpuBus,
- DualBoot,
- Summary,
- Installing,
- Done,
-}
-
-/// Status of an installation step.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum StepStatus {
- Pending,
- Running,
- Done,
- Error,
-}
-
-/// An installation step tracked in the progress view.
-#[derive(Debug, Clone)]
-pub struct InstallStep {
- pub name: String,
- pub label: String,
- pub status: StepStatus,
-}
-
-/// Main application state.
-pub struct App {
- pub page: Page,
- pub should_quit: bool,
-
- // Input state
- pub input: String,
- pub cursor_pos: usize,
- pub err: String,
-
- // Selection state
- pub choices: Vec,
- pub cursor: usize,
-
- // Detected hardware info
- pub detected_disks: Vec,
- pub detected_efis: Vec<(String, String, String)>,
-
- // Collected data
- pub config: InstallConfig,
- pub plain_pw: String,
-
- // Temporary storage for multi-step inputs
- pub part_new_start: String,
-
- // Installation progress
- pub install_steps: Vec,
- pub install_err: Option,
- pub install_rx: Option>,
- pub install_handle: Option>,
- pub spinner_idx: usize,
- pub work_dir: String,
-
- // Live terminal log lines for the UI
- pub log_lines: VecDeque,
-}
-
-impl App {
- pub fn new(work_dir: String) -> Self {
- let mut app = Self {
- page: Page::Welcome,
- should_quit: false,
- input: String::new(),
- cursor_pos: 0,
- err: String::new(),
- choices: Vec::new(),
- cursor: 0,
- detected_disks: Vec::new(),
- detected_efis: Vec::new(),
- config: InstallConfig::default(),
- plain_pw: String::new(),
- part_new_start: String::new(),
- install_steps: vec![
- InstallStep {
- name: "generate_config".into(),
- label: "Generate System Configuration".into(),
- status: StepStatus::Pending,
- },
- InstallStep {
- name: "partition".into(),
- label: "Partition & Format Disk (Disko)".into(),
- status: StepStatus::Pending,
- },
- InstallStep {
- name: "install_nixos".into(),
- label: "Install NixOS System & Packages".into(),
- status: StepStatus::Pending,
- },
- InstallStep {
- name: "copy_flake".into(),
- label: "Copy Flake & Setup User Environment".into(),
- status: StepStatus::Pending,
- },
- ],
- install_err: None,
- install_rx: None,
- install_handle: None,
- spinner_idx: 0,
- work_dir,
- log_lines: VecDeque::with_capacity(100),
- };
- app.init_page();
- app
- }
-
- pub fn add_log(&mut self, line: String) {
- if self.log_lines.len() >= 100 {
- self.log_lines.pop_front();
- }
- self.log_lines.push_back(line);
- }
-
- pub fn apply_profile(&mut self, profile: ProfileChoice) {
- self.config.profile = profile;
- self.config.features = default_features(profile);
- }
-
- pub fn toggle_current_feature(&mut self) {
- if self.cursor < self.config.features.len() {
- self.config.features[self.cursor].enabled = !self.config.features[self.cursor].enabled;
- }
- }
-
- pub fn toggle_current_dual_boot(&mut self) {
- if self.cursor < self.config.dual_boot_entries.len() {
- self.config.dual_boot_entries[self.cursor].enabled =
- !self.config.dual_boot_entries[self.cursor].enabled;
- }
- }
-
- pub fn init_page(&mut self) {
- self.err.clear();
- self.cursor = 0;
-
- match self.page {
- Page::Welcome => {}
- Page::Hostname => {
- self.input = self.config.hostname.clone();
- self.cursor_pos = self.input.len();
- }
- Page::Username => {
- self.input = self.config.username.clone();
- self.cursor_pos = self.input.len();
- }
- Page::Password | Page::PasswordConfirm => {
- self.input.clear();
- self.cursor_pos = 0;
- }
- Page::Profile => {
- self.choices = vec![
- ProfileChoice::Base.to_string(),
- ProfileChoice::Desktop.to_string(),
- ProfileChoice::Workstation.to_string(),
- ];
- self.cursor = match self.config.profile {
- ProfileChoice::Base => 0,
- ProfileChoice::Desktop => 1,
- ProfileChoice::Workstation => 2,
- };
- }
- Page::ProfileCustomize => {
- self.cursor = 0;
- }
- Page::Bootloader => {
- self.choices = vec![
- BootloaderChoice::Grub.to_string(),
- BootloaderChoice::Limine.to_string(),
- ];
- self.cursor = match self.config.bootloader {
- BootloaderChoice::Grub => 0,
- BootloaderChoice::Limine => 1,
- };
- }
- Page::Mode => {
- self.choices = vec![
- "Whole Disk (Disko wipes disk & partitions automatically)".into(),
- "Partition Only (Install alongside existing OS / custom partitions)".into(),
- ];
- self.cursor = if self.config.mode == InstallMode::PartitionOnly {
- 1
- } else {
- 0
- };
- }
- Page::Disk => {
- if !self.detected_disks.is_empty() {
- self.choices = self
- .detected_disks
- .iter()
- .map(|d| format!("{} - {} ({}, {})", d.name, d.size, d.drive_type, d.model))
- .collect();
- if let Some(pos) = self
- .detected_disks
- .iter()
- .position(|d| d.name == self.config.disk_dev)
- {
- self.cursor = pos;
- }
- } else {
- self.input = self.config.disk_dev.clone();
- self.cursor_pos = self.input.len();
- }
- }
- Page::DiskConfirm => {
- self.input.clear();
- self.cursor_pos = 0;
- }
- Page::PartSelect => {
- self.choices = vec![
- "Create new partition in free space (parted)".into(),
- "Use existing unformatted partition".into(),
- ];
- }
- Page::PartNewStart => {
- self.input = "100GB".into();
- self.cursor_pos = self.input.len();
- }
- Page::PartNewEnd => {
- self.input = "100%".into();
- self.cursor_pos = self.input.len();
- }
- Page::PartExist => {
- self.input.clear();
- self.cursor_pos = 0;
- }
- Page::PartConfirm => {
- self.choices = vec![
- "Yes, format this partition for NixOS".into(),
- "No, go back and change partition".into(),
- ];
- }
- Page::Efi => {
- if !self.detected_efis.is_empty() {
- self.choices = self
- .detected_efis
- .iter()
- .map(|(dev, size, uuid)| format!("{dev} ({size}) [UUID: {uuid}]"))
- .collect();
- self.choices
- .push("Enter custom EFI partition manually".into());
- } else {
- self.input = self.config.efi_part.clone();
- self.cursor_pos = self.input.len();
- }
- }
- Page::Fs => {
- self.choices = vec![
- "btrfs (recommended: subvolumes for root, home, nix, log & snapshots)".into(),
- "ext4 (standard single partition)".into(),
- ];
- self.cursor = if self.config.fs_type == "ext4" { 1 } else { 0 };
- }
- Page::RootSize => {
- self.input = self.config.root_size.clone();
- self.cursor_pos = self.input.len();
- }
- Page::Swap => {
- self.input = self.config.swap_size.clone();
- self.cursor_pos = self.input.len();
- }
- Page::SwapPartition => {
- self.input = self.config.swap_partition.clone();
- self.cursor_pos = self.input.len();
- }
- Page::Gpu => {
- self.choices = vec![
- GpuChoice::None.to_string(),
- GpuChoice::Nvidia.to_string(),
- GpuChoice::NvidiaPrime.to_string(),
- ];
- self.cursor = match self.config.gpu_choice {
- GpuChoice::None => 0,
- GpuChoice::Nvidia => 1,
- GpuChoice::NvidiaPrime => 2,
- };
- }
- Page::GpuNvBus => {
- self.input = self.config.nvidia_bus_id.clone();
- self.cursor_pos = self.input.len();
- }
- Page::GpuIgpuType => {
- self.choices = vec!["Intel".into(), "AMD".into()];
- self.cursor = match self.config.igpu_type {
- IgpuType::Intel => 0,
- IgpuType::Amd => 1,
- };
- }
- Page::GpuIgpuBus => {
- self.input = self.config.igpu_bus_id.clone();
- self.cursor_pos = self.input.len();
- }
- Page::DualBoot => {
- self.cursor = 0;
- }
- Page::Summary => {}
- Page::Installing => {}
- Page::Done => {}
- }
- }
-
- pub fn go_to_page(&mut self, next: Page) {
- self.page = next;
- self.init_page();
- }
-
- pub fn prev_page(&self) -> Page {
- match self.page {
- Page::Welcome => Page::Welcome,
- Page::Hostname => Page::Welcome,
- Page::Username => Page::Hostname,
- Page::Password => Page::Username,
- Page::PasswordConfirm => Page::Password,
- Page::Profile => Page::PasswordConfirm,
- Page::ProfileCustomize => Page::Profile,
- Page::Bootloader => Page::ProfileCustomize,
- Page::Mode => Page::Bootloader,
- Page::Disk => Page::Mode,
- Page::DiskConfirm => Page::Disk,
- Page::PartSelect => Page::Disk,
- Page::PartNewStart => Page::PartSelect,
- Page::PartNewEnd => Page::PartNewStart,
- Page::PartExist => Page::PartSelect,
- Page::PartConfirm => {
- if self.part_new_start.is_empty() {
- Page::PartExist
- } else {
- Page::PartNewEnd
- }
- }
- Page::Efi => {
- if self.config.mode == InstallMode::WholeDisk {
- Page::DiskConfirm
- } else {
- Page::PartConfirm
- }
- }
- Page::Fs => Page::Efi,
- Page::RootSize => Page::Fs,
- Page::Swap => Page::Fs,
- Page::SwapPartition => Page::Swap,
- Page::Gpu => {
- if self.config.mode == InstallMode::WholeDisk && self.config.fs_type == "ext4" {
- Page::RootSize
- } else if !self.config.swap_partition.is_empty() {
- Page::SwapPartition
- } else {
- Page::Swap
- }
- }
- Page::GpuNvBus => Page::Gpu,
- Page::GpuIgpuType => Page::GpuNvBus,
- Page::GpuIgpuBus => Page::GpuIgpuType,
- Page::DualBoot => match self.config.gpu_choice {
- GpuChoice::None => Page::Gpu,
- GpuChoice::Nvidia => Page::GpuNvBus,
- GpuChoice::NvidiaPrime => Page::GpuIgpuBus,
- },
- Page::Summary => {
- if !self.config.dual_boot_entries.is_empty() {
- Page::DualBoot
- } else {
- match self.config.gpu_choice {
- GpuChoice::None => Page::Gpu,
- GpuChoice::Nvidia => Page::GpuNvBus,
- GpuChoice::NvidiaPrime => Page::GpuIgpuBus,
- }
- }
- }
- Page::Installing => Page::Summary,
- Page::Done => Page::Done,
- }
- }
-
- pub fn type_char(&mut self, c: char) {
- self.input.insert(self.cursor_pos, c);
- self.cursor_pos += 1;
- self.err.clear();
- }
-
- pub fn delete_char(&mut self) {
- if self.cursor_pos > 0 {
- self.input.remove(self.cursor_pos - 1);
- self.cursor_pos -= 1;
- self.err.clear();
- }
- }
-
- pub fn input_value(&self) -> String {
- self.input.trim().to_string()
- }
-
- pub fn spinner_char(&self) -> &'static str {
- const SPINNER: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
- SPINNER[self.spinner_idx % SPINNER.len()]
- }
-}
-
-impl Drop for App {
- fn drop(&mut self) {
- self.plain_pw.zeroize();
- self.input.zeroize();
- }
-}
diff --git a/installer-rs/src/backend.rs b/installer-rs/src/backend.rs
deleted file mode 100644
index 452c641..0000000
--- a/installer-rs/src/backend.rs
+++ /dev/null
@@ -1,587 +0,0 @@
-use crate::app::{
- BootloaderChoice, GpuChoice, InstallConfig, InstallMode, ProfileChoice, ProgressUpdate,
-};
-use crate::cmd::*;
-use crate::detect::{format_grub_extra_entries, format_limine_extra_entries};
-use crate::state::State;
-use std::fs;
-use std::time::Duration;
-use tokio::io::AsyncWriteExt;
-use tokio::sync::mpsc;
-
-/// Hash a password securely without shell interpolation.
-pub async fn hash_password(pw: &str) -> Result {
- // Try mkpasswd first
- if let Ok(output) = tokio::process::Command::new("mkpasswd")
- .args(["-m", "sha-512", pw])
- .output()
- .await
- {
- if output.status.success() {
- let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
- if !hash.is_empty() {
- return Ok(hash);
- }
- }
- }
-
- // Fallback: pipe password to openssl via stdin
- let mut child = tokio::process::Command::new("openssl")
- .args(["passwd", "-6", "-stdin"])
- .stdin(std::process::Stdio::piped())
- .stdout(std::process::Stdio::piped())
- .stderr(std::process::Stdio::null())
- .spawn()
- .map_err(|e| format!("Failed to spawn openssl: {e}"))?;
-
- if let Some(mut stdin) = child.stdin.take() {
- stdin
- .write_all(pw.as_bytes())
- .await
- .map_err(|e| format!("Failed to write password: {e}"))?;
- }
-
- let output = child
- .wait_with_output()
- .await
- .map_err(|e| format!("openssl failed: {e}"))?;
-
- let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
- if hash.is_empty() {
- Err("No password hashing tool found (mkpasswd, openssl)".into())
- } else {
- Ok(hash)
- }
-}
-
-pub fn build_gpu_config(cfg: &InstallConfig) -> String {
- match cfg.gpu_choice {
- GpuChoice::None => String::new(),
- GpuChoice::Nvidia => " # NVIDIA GPU\n northstar.nvidia.enable = true;".to_string(),
- GpuChoice::NvidiaPrime => {
- let key = cfg.igpu_type.bus_id_key();
- format!(
- " # NVIDIA GPU\n northstar.nvidia.enable = true;\n northstar.nvidia.prime = {{\n enable = true;\n nvidiaBusId = \"{}\";\n {key} = \"{}\";\n }};",
- cfg.nvidia_bus_id, cfg.igpu_bus_id
- )
- }
- }
-}
-
-pub fn build_bootloader_config(cfg: &InstallConfig) -> String {
- match cfg.bootloader {
- BootloaderChoice::Grub => {
- let mut s =
- " # Bootloader\n northstar.features.boot.loader = \"grub\";\n".to_string();
- let extra = format_grub_extra_entries(&cfg.dual_boot_entries);
- if !extra.is_empty() {
- s.push_str(&format!("{extra}\n"));
- }
- s
- }
- BootloaderChoice::Limine => {
- let mut s =
- " # Bootloader\n northstar.features.boot.loader = \"limine\";\n".to_string();
- let extra = format_limine_extra_entries(&cfg.dual_boot_entries);
- if !extra.is_empty() {
- s.push_str(&format!("{extra}\n"));
- }
- s
- }
- }
-}
-
-pub fn build_profile_config(cfg: &InstallConfig) -> String {
- let mut lines = vec![
- " # Northstar profiles".to_string(),
- " northstar.profiles = {".to_string(),
- ];
-
- match cfg.profile {
- ProfileChoice::Base => {
- lines.push(" base.enable = true;".into());
- }
- ProfileChoice::Desktop => {
- lines.push(" desktop.enable = true;".into());
- }
- ProfileChoice::Workstation => {
- lines.push(" desktop.enable = true;".into());
- lines.push(" workstation.enable = true;".into());
- }
- }
- lines.push(" };".into());
- lines.join("\n")
-}
-
-pub fn build_features_override(cfg: &InstallConfig) -> String {
- let defaults = crate::app::default_features(cfg.profile);
- let mut overrides = Vec::new();
-
- for custom in &cfg.features {
- if let Some(def) = defaults.iter().find(|d| d.id == custom.id) {
- if def.enabled != custom.enabled {
- overrides.push(format!(" {}.enable = {};", custom.id, custom.enabled));
- }
- }
- }
-
- if overrides.is_empty() {
- return String::new();
- }
-
- format!(
- " # Custom feature overrides\n northstar.features = {{\n{}\n }};",
- overrides.join("\n")
- )
-}
-
-/// Strip fileSystems and swapDevices entries from hardware.nix output.
-pub fn strip_filesystems_from_hardware(hw_text: &str) -> String {
- let mut cleaned_lines = Vec::new();
- let mut in_fs_block = false;
- let mut fs_depth: i32 = 0;
- let mut in_swap_devices = false;
-
- for line in hw_text.lines() {
- let stripped = line.trim();
-
- if in_swap_devices {
- if stripped.contains(';') {
- in_swap_devices = false;
- }
- continue;
- }
-
- if stripped.starts_with("swapDevices") {
- if !stripped.contains(';') {
- in_swap_devices = true;
- }
- continue;
- }
-
- if stripped.starts_with("fileSystems.") || stripped.starts_with("fileSystems =") {
- in_fs_block = true;
- fs_depth += stripped.matches('{').count() as i32 - stripped.matches('}').count() as i32;
- if stripped.contains(';') || (fs_depth == 0 && stripped.contains('}')) {
- in_fs_block = false;
- fs_depth = 0;
- }
- continue;
- }
-
- if in_fs_block {
- fs_depth += stripped.matches('{').count() as i32 - stripped.matches('}').count() as i32;
- if fs_depth <= 0 && (stripped.contains('}') || stripped.contains(';')) {
- in_fs_block = false;
- fs_depth = 0;
- }
- continue;
- }
-
- cleaned_lines.push(line);
- }
-
- let mut result = cleaned_lines.join("\n");
- while result.contains("\n\n\n") {
- result = result.replace("\n\n\n", "\n\n");
- }
- result.trim().to_string()
-}
-
-/// Generate disko.nix content for whole-disk mode.
-pub fn generate_disko_whole_disk(cfg: &InstallConfig) -> String {
- let template = if cfg.fs_type == "ext4" {
- "ext4"
- } else {
- "btrfs"
- };
- let mut disko = format!(
- "# Auto-generated disko config for {}\n{{ lib, ... }}:\n{{\n imports = [ ../../lib/disko/{template}.nix ];\n\n disko.devices.disk.main.device = \"/dev/{}\";\n",
- cfg.hostname, cfg.disk_dev
- );
-
- if cfg.swap_size == "0" {
- disko += " # Swap disabled\n disko.devices.disk.main.content.partitions.swap.size = lib.mkForce \"0\";\n";
- } else if cfg.swap_size != "8G" {
- disko += &format!(
- " disko.devices.disk.main.content.partitions.swap.size = lib.mkForce \"{}\";\n",
- cfg.swap_size
- );
- }
-
- if cfg.root_size != "100%" {
- disko += &format!(
- " disko.devices.disk.main.content.partitions.root.size = lib.mkForce \"{}\";\n",
- cfg.root_size
- );
- }
-
- disko += "}\n";
- disko
-}
-
-/// Generate disko.nix content for partition-only mode.
-pub async fn generate_disko_partition_only(cfg: &InstallConfig) -> String {
- let np = &cfg.nixos_part;
- let ep = &cfg.efi_part;
-
- let efi_uuid = run_capture(&format!("blkid -s UUID -o value {ep}"))
- .await
- .unwrap_or_default();
-
- let mut lines = vec![
- format!(
- "# Auto-generated disko config for {} (partition-only)",
- cfg.hostname
- ),
- "{".to_string(),
- format!(" disko.devices.disk.nixos = {{"),
- format!(" type = \"disk\";"),
- format!(" device = \"{np}\";"),
- format!(" content = {{"),
- ];
-
- if cfg.fs_type == "btrfs" {
- lines.push(" type = \"btrfs\";".into());
- lines.push(" extraArgs = [ \"-f\" ];".into());
- lines.push(" subvolumes = {".into());
- lines.push(" \"/root\" = {".into());
- lines.push(" mountpoint = \"/\";".into());
- lines.push(" mountOptions = [ \"compress=zstd\" ];".into());
- lines.push(" };".into());
- lines.push(" \"/home\" = {".into());
- lines.push(" mountpoint = \"/home\";".into());
- lines.push(" mountOptions = [ \"compress=zstd\" ];".into());
- lines.push(" };".into());
- lines.push(" \"/nix\" = {".into());
- lines.push(" mountpoint = \"/nix\";".into());
- lines.push(" mountOptions = [ \"compress=zstd\" \"noatime\" ];".into());
- lines.push(" };".into());
- lines.push(" \"/log\" = {".into());
- lines.push(" mountpoint = \"/var/log\";".into());
- lines.push(" mountOptions = [ \"compress=zstd\" ];".into());
- lines.push(" };".into());
- if cfg.swap_size != "0" {
- lines.push(" \"/swap\" = {".into());
- lines.push(" mountpoint = \"/swap\";".into());
- lines.push(" };".into());
- }
- lines.push(" };".into());
- } else {
- lines.push(" type = \"filesystem\";".into());
- lines.push(" format = \"ext4\";".into());
- lines.push(" mountpoint = \"/\";".into());
- }
-
- lines.push(" };".into());
- lines.push(" };".into());
-
- if cfg.swap_size != "0" && cfg.fs_type == "ext4" && !cfg.swap_partition.is_empty() {
- lines.push(String::new());
- lines.push(" disko.devices.disk.swap = {".into());
- lines.push(" type = \"disk\";".into());
- lines.push(format!(" device = \"{}\";", cfg.swap_partition));
- lines.push(" content = {".into());
- lines.push(" type = \"swap\";".into());
- lines.push(" discardPolicy = \"both\";".into());
- lines.push(" resumeDevice = true;".into());
- lines.push(" };".into());
- lines.push(" };".into());
- }
-
- lines.push(String::new());
- lines.push(" # Existing EFI partition — not managed by disko".into());
-
- if !efi_uuid.is_empty() {
- lines.push(" fileSystems.\"/boot/efi\" = {".into());
- lines.push(format!(" device = \"/dev/disk/by-uuid/{efi_uuid}\";"));
- lines.push(" fsType = \"vfat\";".into());
- lines.push(" options = [ \"fmask=0022\" \"dmask=0022\" ];".into());
- lines.push(" };".into());
- } else {
- lines.push(" fileSystems.\"/boot/efi\" = {".into());
- lines.push(format!(" device = \"{ep}\";"));
- lines.push(" fsType = \"vfat\";".into());
- lines.push(" options = [ \"fmask=0022\" \"dmask=0022\" ];".into());
- lines.push(" };".into());
- }
-
- if cfg.swap_size != "0" && cfg.fs_type == "btrfs" {
- lines.push(String::new());
- lines.push(" swapDevices = [".into());
- lines.push(" { device = \"/swap/swapfile\"; }".into());
- lines.push(" ];".into());
- }
-
- lines.push("}".into());
- lines.push(String::new());
-
- lines.join("\n")
-}
-
-pub fn generate_host_default_nix(cfg: &InstallConfig) -> String {
- let bootloader_config = build_bootloader_config(cfg);
- let profiles_config = build_profile_config(cfg);
- let features_config = build_features_override(cfg);
- let gpu_config = build_gpu_config(cfg);
-
- let mut blocks = Vec::new();
- if !bootloader_config.is_empty() {
- blocks.push(bootloader_config.trim_end().to_string());
- }
-
- let user_block = format!(
- r#" users.users.{user} = {{
- isNormalUser = true;
- description = "{user}";
- extraGroups = [
- "networkmanager"
- "wheel"
- "libvirtd"
- "docker"
- ];
- shell = pkgs.{shell};
- hashedPassword = "{pw}";
- }};"#,
- user = cfg.username,
- shell = cfg.shell,
- pw = cfg.hashed_pw,
- );
- blocks.push(user_block);
-
- blocks.push(profiles_config);
-
- if !features_config.is_empty() {
- blocks.push(features_config);
- }
-
- if !gpu_config.is_empty() {
- blocks.push(gpu_config);
- }
-
- let body = blocks.join("\n\n");
-
- format!(
- r#"{{
- config,
- lib,
- pkgs,
- ...
-}}:
-
-{{
- imports = [
- ./disko.nix
- ];
-
- home-manager.users.{user} = {{
- imports = [ ../../home ];
- home.username = lib.mkForce "{user}";
- home.homeDirectory = lib.mkForce "/home/{user}";
- }};
-
-{body}
-
- networking.hostName = "{host}";
- system.stateVersion = "26.11";
-}}
-"#,
- user = cfg.username,
- body = body,
- host = cfg.hostname,
- )
-}
-
-pub async fn generate_config(cfg: &InstallConfig, work_dir: &str) -> Result<(), String> {
- let host_dir = format!("{work_dir}/hosts/{}", cfg.hostname);
- let _ = fs::create_dir_all(&host_dir);
-
- // Write disko.nix
- let disko_content = if cfg.mode == InstallMode::WholeDisk {
- generate_disko_whole_disk(cfg)
- } else {
- generate_disko_partition_only(cfg).await
- };
- fs::write(format!("{host_dir}/disko.nix"), &disko_content)
- .map_err(|e| format!("Failed to write disko.nix: {e}"))?;
-
- // Write default.nix
- let default_content = generate_host_default_nix(cfg);
- fs::write(format!("{host_dir}/default.nix"), &default_content)
- .map_err(|e| format!("Failed to write default.nix: {e}"))?;
-
- // Track files in git for Nix flakes
- run_silent("git add -A").await;
- Ok(())
-}
-
-/// Run all installation steps, sending progress updates through the channel.
-pub async fn run_installation(
- cfg: InstallConfig,
- state: &mut State,
- work_dir: &str,
- tx: mpsc::UnboundedSender,
-) {
- let send = |step: &str, msg: &str| {
- let _ = tx.send(ProgressUpdate {
- step: step.into(),
- message: msg.into(),
- done: false,
- error: None,
- });
- };
- let done = |step: &str, msg: &str| {
- let _ = tx.send(ProgressUpdate {
- step: step.into(),
- message: msg.into(),
- done: true,
- error: None,
- });
- };
- let fail = |step: &str, err: String| {
- let _ = tx.send(ProgressUpdate {
- step: step.into(),
- message: String::new(),
- done: false,
- error: Some(err),
- });
- };
-
- // Step 1: Generate config
- send("generate_config", "Generating system configuration...");
- if !state.should_skip("generate_config") {
- if let Err(e) = generate_config(&cfg, work_dir).await {
- fail("generate_config", e);
- return;
- }
- state.set_step("partition");
- }
- done("generate_config", "Configuration generated");
-
- // Step 2: Partition
- send("partition", "Partitioning disk with Disko...");
- if !state.should_skip("partition") {
- if let Err(e) = do_partition(&cfg, work_dir).await {
- fail("partition", e);
- return;
- }
- state.set_step("install_nixos");
- }
- done("partition", "Disk partitioned & formatted");
-
- // Step 3: Install NixOS
- send("install_nixos", "Building & installing NixOS system...");
- if !state.should_skip("install_nixos") {
- if let Err(e) = do_install_nixos(&cfg).await {
- fail("install_nixos", e);
- return;
- }
- state.set_step("copy_flake");
- }
- done("install_nixos", "NixOS system installed");
-
- // Step 4: Copy Flake
- send(
- "copy_flake",
- "Copying configuration flake to target user...",
- );
- if !state.should_skip("copy_flake") {
- if let Err(e) = do_copy_flake(&cfg, work_dir).await {
- fail("copy_flake", e);
- return;
- }
- state.set_step("done");
- }
- done("copy_flake", "Flake copied to target user home");
-
- state.clear();
-}
-
-async fn do_partition(cfg: &InstallConfig, work_dir: &str) -> Result<(), String> {
- let host_dir = format!("{work_dir}/hosts/{}", cfg.hostname);
-
- retry("partition", 3, Duration::from_secs(5), || async {
- run(&format!(
- r#"nix run github:nix-community/disko -- --mode disko --flake ".#{}""#,
- cfg.hostname
- ))
- .await
- })
- .await?;
-
- if cfg.mode == InstallMode::PartitionOnly {
- let _ = fs::create_dir_all("/mnt/boot/efi");
- if !is_mounted("/mnt/boot/efi").await {
- run(&format!("mount {} /mnt/boot/efi", cfg.efi_part)).await?;
- }
-
- if cfg.swap_size != "0" && cfg.fs_type == "btrfs" && !path_exists("/mnt/swap/swapfile") {
- run_silent("chattr +C /mnt/swap").await;
- let _ = run("truncate -s 0 /mnt/swap/swapfile").await;
- run_silent("chattr +C /mnt/swap/swapfile").await;
- let _ = run(&format!(
- "fallocate -l {} /mnt/swap/swapfile",
- cfg.swap_size
- ))
- .await;
- let _ = run("chmod 600 /mnt/swap/swapfile").await;
- let _ = run("mkswap /mnt/swap/swapfile").await;
- let _ = run("swapon /mnt/swap/swapfile").await;
- }
-
- let hw = run_capture("nixos-generate-config --root /mnt --show-hardware-config").await?;
- let hw = strip_filesystems_from_hardware(&hw);
- let _ = fs::write(format!("{host_dir}/hardware.nix"), format!("{hw}\n"));
- run_silent("git add -A").await;
- }
-
- Ok(())
-}
-
-async fn do_install_nixos(cfg: &InstallConfig) -> Result<(), String> {
- retry("nixos-install", 3, Duration::from_secs(10), || {
- let host = cfg.hostname.clone();
- async move {
- run(&format!(
- r#"nixos-install --flake ".#{host}" --no-root-password"#
- ))
- .await
- }
- })
- .await
-}
-
-async fn do_copy_flake(cfg: &InstallConfig, work_dir: &str) -> Result<(), String> {
- let dest = format!("/mnt/home/{}/northstar", cfg.username);
- let hostname = cfg.hostname.clone();
- let username = cfg.username.clone();
- let wd = work_dir.to_string();
-
- retry("copy-flake", 3, Duration::from_secs(5), || {
- let dest = dest.clone();
- let hostname = hostname.clone();
- let username = username.clone();
- let wd = wd.clone();
- async move {
- let _ = fs::remove_dir_all(&dest);
- run(&format!("cp -a {wd}/. {dest}/")).await?;
- let _ = fs::remove_dir_all(format!("{dest}/.git"));
- run(&format!(
- r#"cd {dest} && git init && git config user.name "Northstar Installer" && git config user.email "installer@northstar.local" && git add -A && git commit -m "Initial Northstar configuration for {hostname}""#
- )).await?;
-
- // Fix ownership
- if let Ok(passwd) = fs::read_to_string("/mnt/etc/passwd") {
- for line in passwd.lines() {
- let fields: Vec<&str> = line.split(':').collect();
- if fields.len() > 3 && fields[0] == username {
- let (uid, gid) = (fields[2], fields[3]);
- let _ = run(&format!("chown -R {uid}:{gid} {dest}")).await;
- return Ok(());
- }
- }
- }
- Ok(())
- }
- }).await
-}
diff --git a/installer-rs/src/cmd.rs b/installer-rs/src/cmd.rs
deleted file mode 100644
index eb18af6..0000000
--- a/installer-rs/src/cmd.rs
+++ /dev/null
@@ -1,119 +0,0 @@
-use std::path::Path;
-use std::time::Duration;
-use tokio::process::Command;
-
-/// Run a shell command, returning error with output on failure.
-pub async fn run(cmd: &str) -> Result<(), String> {
- let output = Command::new("sh")
- .arg("-c")
- .arg(cmd)
- .output()
- .await
- .map_err(|e| format!("Failed to execute: {e}"))?;
-
- if output.status.success() {
- Ok(())
- } else {
- let stderr = String::from_utf8_lossy(&output.stderr);
- let stdout = String::from_utf8_lossy(&output.stdout);
- Err(format!(
- "Command failed ({}): {} {}",
- output.status, stdout, stderr
- ))
- }
-}
-
-/// Run a shell command and capture stdout.
-pub async fn run_capture(cmd: &str) -> Result {
- let output = Command::new("sh")
- .arg("-c")
- .arg(cmd)
- .output()
- .await
- .map_err(|e| format!("Failed to execute: {e}"))?;
-
- Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
-}
-
-/// Run a shell command, ignoring errors.
-pub async fn run_silent(cmd: &str) {
- let _ = Command::new("sh").arg("-c").arg(cmd).output().await;
-}
-
-/// Check if a path is a mount point.
-pub async fn is_mounted(path: &str) -> bool {
- Command::new("mountpoint")
- .arg("-q")
- .arg(path)
- .status()
- .await
- .map(|s| s.success())
- .unwrap_or(false)
-}
-
-/// Check if a device has a filesystem.
-#[allow(dead_code)]
-pub async fn has_filesystem(device: &str) -> bool {
- run_capture(&format!("blkid -o value -s TYPE {device}"))
- .await
- .map(|s| !s.is_empty())
- .unwrap_or(false)
-}
-
-/// Get filesystem type of a device.
-#[allow(dead_code)]
-pub async fn get_filesystem(device: &str) -> String {
- run_capture(&format!("blkid -o value -s TYPE {device}"))
- .await
- .unwrap_or_default()
-}
-
-/// Check if a btrfs subvolume exists.
-#[allow(dead_code)]
-pub async fn subvolume_exists(mount: &str, name: &str) -> bool {
- run_capture(&format!("btrfs subvolume list {mount}"))
- .await
- .map(|s| s.contains(name))
- .unwrap_or(false)
-}
-
-/// Check if a path exists.
-pub fn path_exists(path: &str) -> bool {
- Path::new(path).exists()
-}
-
-/// Validate a disk device name — reject path traversal attempts.
-#[allow(dead_code)]
-pub fn validate_device_name(name: &str) -> Result<(), String> {
- if name.contains("..") || name.contains('/') {
- return Err("Invalid device name: must not contain '..' or '/'".into());
- }
- if name.is_empty() {
- return Err("Device name cannot be empty".into());
- }
- Ok(())
-}
-
-/// Retry a function with exponential backoff, capped to prevent overflow.
-pub async fn retry(name: &str, max: usize, base_delay: Duration, f: F) -> Result<(), String>
-where
- F: Fn() -> Fut,
- Fut: std::future::Future