commit 97c865d91b32510cfc6388736cb3efef86d998f3 Author: randogoth Date: Sun Feb 1 19:43:03 2026 +0200 Add Lix-based NixOS installer ISO and interactive script diff --git a/agents.md b/agents.md new file mode 100644 index 0000000..230ead7 --- /dev/null +++ b/agents.md @@ -0,0 +1,287 @@ +# agents.md — NixOS (Lix) Configuration Agent + +## Role + +You are an expert **NixOS configuration agent** specializing in: + +- **Flake-first NixOS** +- **Lix-flavored Nix (preferred but not mandatory)** +- **Modular NixOS + Home Manager systems** +- **Flake-contained host modules (“configuration.nix-style”)** +- Deterministic, reproducible configurations + +You assist by **editing or proposing Nix code**. +You do **not** explain Nix concepts unless explicitly requested. + +--- + +## Authority Model + +1. **`flake.nix` is the sole entry point** + - All evaluation flows through `outputs` + - No channel-based workflows + - No implicit `NIX_PATH` + - No reliance on `/etc/nixos` + +2. **All systems are flakes** + - Legacy `configuration.nix` as an entry point is forbidden + - Files that *look* like `configuration.nix` are allowed **only as flake-contained host modules** + +--- + +## Required Workflow + +### Before Making Any Changes + +You **must present a concise plan** and wait for confirmation. + +The plan must include: +- Bullet points only +- Exact file paths to be touched +- High-level intent per file +- No code +- No prose explanations + +Example: + +```text +Plan: +- Add flake.nix with pinned nixpkgs +- Add hosts/laptop.nix as host module +- Add modules/system/base.nix +- Wire host via nixosConfigurations +```` + +You may **only modify files listed in the approved plan**. + +Scope is **strict**. + +--- + +## Flake-Contained `configuration.nix`-Style Modules (Host Modules) + +### Definition + +A **host module** is a NixOS module that: + +* Has the standard module signature: + + ```nix + { config, pkgs, ... }: + ``` +* Looks like a traditional `configuration.nix` +* Is **not** an entry point +* Is **only evaluated via `flake.nix`** +* Exists solely to **compose a specific machine** + +This pattern is **explicitly allowed and encouraged** when used correctly. + +--- + +### Allowed Responsibilities (Host Modules) + +Host modules **may**: + +* Compose the system via `imports` +* Set host-specific values: + + * `networking.hostName` + * `system.stateVersion` + * locale / timezone +* Apply small, truly host-unique overrides + +Example: + +```nix +# hosts/laptop.nix +{ config, pkgs, ... }: + +{ + imports = [ + ../modules/system/base.nix + ../modules/desktop/wayland.nix + ../modules/users/tlalit.nix + ]; + + networking.hostName = "laptop"; + system.stateVersion = "24.11"; +} +``` + +--- + +### Forbidden Responsibilities (Host Modules) + +Host modules **must not**: + +* Implement reusable features +* Contain large logic blocks +* Define users inline +* Enable services that could apply to more than one host +* Act as monolithic system definitions + +Rule of thumb (enforced): + +> **Host modules compose. Feature modules implement.** + +If a setting could plausibly apply to more than one host, it does **not** belong in a host module. + +--- + +## Module Categories + +### Host Modules + +* Path: `hosts/*.nix` +* Role: composition only +* Small, declarative +* No reusable logic + +### Feature Modules + +* Path: `modules/**` +* Role: implementation +* Reusable +* Use upstream NixOS options only +* **No custom option namespaces** + +Example feature module skeleton: + +```nix +{ config, lib, pkgs, ... }: + +{ + config = { + # implementation using upstream options + }; +} +``` + +--- + +## Home Manager Policy + +* **Home Manager must be integrated as a NixOS module** +* No standalone Home Manager flakes +* No per-user Home Manager flakes +* User configuration lives under `modules/users/` + +--- + +## Hardware Policy + +* GPU support and Btrfs support are desired +* Hardware modules are allowed +* **Hardware changes must be explicitly included in the plan** +* No surprise disk, bootloader, or kernel changes + +--- + +## Overlays Policy + +* Overlays are **allowed but discouraged** +* The agent **must not introduce overlays unless explicitly requested** +* Prefer: + + * explicit flake inputs + * local `callPackage` + * direct package references + +No proactive overlay usage. + +--- + +## Code Style Rules + +### Nix + +* Pure Nix only +* Prefer explicit attribute paths +* Prefer: + + * `lib.mkIf` + * `lib.mkMerge` + * `lib.optionals` +* Avoid: + + * `with pkgs;` + * implicit imports + * inline shell hacks + +### Formatting + +* All Nix code must conform to **`nixfmt-rfc-style`** +* Do not reformat unrelated files + +--- + +## Scripting Policy + +1. **Prefer Python 3** + + * Scripts must be deterministic and non-interactive + * Stored under `./scripts/` + * May run **at activation time only** + * Not at evaluation time + * Not at build time unless explicitly requested + +2. **Shell scripts** + + * Allowed only when unavoidable + * POSIX-compliant + * Minimal + * Generated via `writeShellScriptBin` if needed + +3. **Never embed large scripts inline** + +--- + +## Expected Repository Layout + +```text +. +├── flake.nix +├── flake.lock +├── hosts/ +│ └── hostname.nix +├── modules/ +│ ├── system/ +│ ├── hardware/ +│ ├── services/ +│ ├── desktop/ +│ ├── users/ +│ └── development/ +├── scripts/ +└── lib/ +``` + +* `flake.nix` → authority +* `hosts/` → composition +* `modules/` → behavior + +--- + +## Safety & Reproducibility + +* No imperative installs +* No network access at evaluation +* Inputs must be pinned +* All changes must be declarative + +--- + +## Output Rules + +* Plans: **plan only** +* Code: **code only** +* Questions: **one precise question only** + +``` + +--- + +If you want, next we can: +- Add **machine-checkable lint rules** derived from this +- Write a **migration appendix** for legacy `/etc/nixos` +- Create a **Codex system prompt** that mirrors this file exactly +``` \ No newline at end of file diff --git a/docs/install-help.txt b/docs/install-help.txt new file mode 100644 index 0000000..c3f4fc8 --- /dev/null +++ b/docs/install-help.txt @@ -0,0 +1,17 @@ +NixOS installer (flake + Lix) +============================= + +1) Build is prewired with Lix; the installer ISO includes the repo. +2) Run the guided Btrfs installer: + + sudo ./scripts/install-btrfs.sh + + - Interactive by default (choose disk, LUKS, swap). + - Non-interactive example: + sudo NON_INTERACTIVE=1 USE_LUKS=1 LUKS_PASSPHRASE='secret' SWAP_GB=4 DISK=/dev/sda HOSTNAME=myhost ./scripts/install-btrfs.sh + +3) After install finishes, reboot into the new system. + +Notes +- Filesystem is always Btrfs with subvolumes: root, home, nix (and swapfile if enabled). +- The flake lives under /etc/nixos on the target; edit hosts/.nix to customize. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..d86eabe --- /dev/null +++ b/flake.lock @@ -0,0 +1,113 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flakey-profile": { + "locked": { + "lastModified": 1712898590, + "narHash": "sha256-FhGIEU93VHAChKEXx905TSiPZKga69bWl1VB37FK//I=", + "owner": "lf-", + "repo": "flakey-profile", + "rev": "243c903fd8eadc0f63d205665a92d4df91d42d9d", + "type": "github" + }, + "original": { + "owner": "lf-", + "repo": "flakey-profile", + "type": "github" + } + }, + "lix": { + "flake": false, + "locked": { + "lastModified": 1761937274, + "narHash": "sha256-KlELhsSq3XbemrGyQhmGurFu7m8wOEBw+8M04L7hn7A=", + "rev": "91867941fa73afea7869b7c71ede82e5ef8927da", + "type": "tarball", + "url": "https://git.lix.systems/api/v1/repos/lix-project/lix/archive/91867941fa73afea7869b7c71ede82e5ef8927da.tar.gz?rev=91867941fa73afea7869b7c71ede82e5ef8927da" + }, + "original": { + "type": "tarball", + "url": "https://git.lix.systems/lix-project/lix/archive/main.tar.gz" + } + }, + "lix-module": { + "inputs": { + "flake-utils": "flake-utils", + "flakey-profile": "flakey-profile", + "lix": "lix", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1767364176, + "narHash": "sha256-l6YdEBYQxXjD8ujqvc0tKdwWc3K8UQOi+E4Y3DKQ318=", + "ref": "refs/heads/main", + "rev": "1688100bba140492658d597f6b307c327f35c780", + "revCount": 179, + "type": "git", + "url": "https://git.lix.systems/lix-project/nixos-module" + }, + "original": { + "type": "git", + "url": "https://git.lix.systems/lix-project/nixos-module" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1769741972, + "narHash": "sha256-RxSg1EioTWNpoLaykiT1UQKTo/K0PPdLqCyQgNjNqWs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "63590ac958a8af30ebd52c7a0309d8c52a94dd77", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "lix-module": "lix-module", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d201a9d --- /dev/null +++ b/flake.nix @@ -0,0 +1,29 @@ +{ + description = "NixOS installation ISO with Lix"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + lix-module.url = "git+https://git.lix.systems/lix-project/nixos-module"; + lix-module.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = inputs@{ self, nixpkgs, ... }: + let + lib = nixpkgs.lib; + hostFiles = builtins.attrNames (builtins.readDir ./hosts); + hostNames = lib.filter (name: + lib.hasSuffix ".nix" name && !(lib.hasSuffix "-hardware.nix" name) + ) hostFiles; + mkHost = name: lib.nixosSystem { + system = "x86_64-linux"; + specialArgs = { inherit inputs; }; + modules = [ (./hosts + "/${name}.nix") ]; + }; + in { + nixosConfigurations = lib.genAttrs (map (lib.removeSuffix ".nix") hostNames) mkHost; + + packages.x86_64-linux.install-iso = + self.nixosConfigurations.installer.config.system.build.isoImage; + packages.x86_64-linux.default = self.packages.x86_64-linux.install-iso; + }; +} diff --git a/hosts/installer.nix b/hosts/installer.nix new file mode 100644 index 0000000..1d95935 --- /dev/null +++ b/hosts/installer.nix @@ -0,0 +1,14 @@ +{ config, pkgs, inputs, ... }: + +{ + imports = [ + (inputs.nixpkgs + "/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix") + inputs.lix-module.nixosModules.lixFromNixpkgs + ../modules/system/base.nix + ]; + + networking.hostName = "nix-installer"; + + environment.etc."INSTALL.txt".source = ../docs/install-help.txt; + environment.etc."motd".text = builtins.readFile ../docs/install-help.txt; +} diff --git a/modules/system/base.nix b/modules/system/base.nix new file mode 100644 index 0000000..d11967d --- /dev/null +++ b/modules/system/base.nix @@ -0,0 +1,26 @@ +{ config, lib, pkgs, ... }: + +{ + time.timeZone = "UTC"; + + i18n.defaultLocale = "en_US.UTF-8"; + console.keyMap = "us"; + + nix.settings = { + experimental-features = [ "nix-command" "flakes" ]; + substituters = lib.mkBefore [ + "https://cache.lix.systems" + ]; + trusted-public-keys = lib.mkBefore [ + "cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o=" + ]; + }; + + services.openssh.enable = true; + + networking.useDHCP = lib.mkDefault true; + + boot.supportedFilesystems = [ "btrfs" "ext4" "vfat" "xfs" ]; + + system.stateVersion = "25.11"; +} diff --git a/scripts/install-btrfs.sh b/scripts/install-btrfs.sh new file mode 100755 index 0000000..99cabd5 --- /dev/null +++ b/scripts/install-btrfs.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Automated Btrfs install using the flake in this repo. +# Defaults remain environment-driven for headless use. +# Interactive prompts are shown unless --non-interactive is set. + +NON_INTERACTIVE=0 +if [[ "${1:-}" == "--non-interactive" ]]; then + NON_INTERACTIVE=1 + shift +fi + +DISK=${DISK:-/dev/vda} # full-disk target +ROOT_PART=${ROOT_PART:-} # optional existing root partition +BOOT_PART=${BOOT_PART:-} # optional existing boot (FAT) partition +HOSTNAME=${HOSTNAME:-nixos} +USE_LUKS=${USE_LUKS:-0} # 1 to encrypt root +SWAP_GB=${SWAP_GB:-0} # swap size in GB, 0 disables + +SCRIPT_DIR=$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd -- "${SCRIPT_DIR}/.." && pwd) + +has_cmd() { command -v "$1" >/dev/null 2>&1; } + +prompt_select() { + local title="$1" prompt="$2" choices=("${@:3}") + if (( NON_INTERACTIVE )); then + echo "${choices[0]}" + return + fi + if has_cmd dialog; then + dialog --no-cancel --menu "$prompt" 15 60 6 "${choices[@]}" 2> /tmp/choice + cat /tmp/choice + elif has_cmd whiptail; then + whiptail --notags --menu "$prompt" 15 60 6 "${choices[@]}" 2> /tmp/choice || true + cat /tmp/choice + else + echo "$prompt" + select c in "${choices[@]}"; do echo "$c"; break; done + fi +} + +prompt_yes_no() { + local prompt="$1" default="${2:-n}" + if (( NON_INTERACTIVE )); then + [[ "$default" =~ ^[Yy]$ ]] && echo "yes" || echo "no" + return + fi + if has_cmd dialog; then + if dialog --yesno "$prompt" 8 60; then echo "yes"; else echo "no"; fi + elif has_cmd whiptail; then + if whiptail --yesno "$prompt" 8 60; then echo "yes"; else echo "no"; fi + else + read -rp "$prompt [y/N]: " ans + [[ "${ans:-$default}" =~ ^[Yy]$ ]] && echo "yes" || echo "no" + fi +} + +prompt_input() { + local prompt="$1" default="$2" + if (( NON_INTERACTIVE )); then + echo "$default" + return + fi + if has_cmd dialog; then + dialog --inputbox "$prompt" 8 60 "$default" 2>/tmp/input && cat /tmp/input || echo "$default" + elif has_cmd whiptail; then + whiptail --inputbox "$prompt" 8 60 "$default" 2>/tmp/input && cat /tmp/input || echo "$default" + else + read -rp "$prompt [$default]: " ans + echo "${ans:-$default}" + fi +} + +ls_disks() { + lsblk -dpno NAME,SIZE,TYPE | awk '$3=="disk"{print $1" "$2}' +} + +if (( ! NON_INTERACTIVE )); then + mapfile -t disks < <(ls_disks) + if ((${#disks[@]})); then + disk_choices=() + for d in "${disks[@]}"; do + disk_choices+=("$d" "") + done + DISK=$(prompt_select "disk" "Select target disk (will be wiped)" "${disk_choices[@]}") + fi + + target_mode=$(prompt_select "mode" "Partitioning mode" \ + "full-disk" "Use entire disk (wipes!)" \ + "existing" "Use existing partitions") + + if [[ "$target_mode" == "existing" ]]; then + ROOT_PART=$(prompt_input "Root partition (will be formatted btrfs)" "${ROOT_PART:-/dev/vda2}") + BOOT_PART=$(prompt_input "Boot EFI partition (FAT32)" "${BOOT_PART:-/dev/vda1}") + fi + + USE_LUKS=$(prompt_yes_no "Encrypt root with LUKS?" "n") + [[ "$USE_LUKS" == "yes" ]] && USE_LUKS=1 || USE_LUKS=0 + + SWAP_GB=$(prompt_input "Swap size in GB (0 = none)" "${SWAP_GB}") + HOSTNAME=$(prompt_input "Hostname" "${HOSTNAME}") +fi + +echo "[0/8] Summary:" +echo " Disk: ${DISK}" +if [[ -n "$ROOT_PART" ]]; then + echo " Root partition: $ROOT_PART" + echo " Boot partition: $BOOT_PART" +else + echo " Mode: full disk wipe" +fi +echo " LUKS: $([ "$USE_LUKS" -eq 1 ] && echo enabled || echo disabled)" +echo " Swap: ${SWAP_GB}G" +echo " Hostname: ${HOSTNAME}" + +if (( NON_INTERACTIVE )); then + echo "Proceeding in non-interactive mode." +else + if [[ "$(prompt_yes_no "Proceed with these settings? This will destroy data." "n")" != "yes" ]]; then + echo "Aborted." + exit 1 + fi +fi + +part_boot="" +part_root="" +mapper_root="" + +echo "[1/8] Partitioning/selection" +if [[ -z "$ROOT_PART" ]]; then + printf "label: gpt\n,550M,U,boot\n,,L,cryptroot\n" | sfdisk "${DISK}" + part_boot="${DISK}1" + part_root="${DISK}2" +else + part_boot="${BOOT_PART}" + part_root="${ROOT_PART}" +fi + +echo "[2/8] Formatting boot" +mkfs.fat -F 32 "${part_boot}" + +echo "[3/8] Preparing root" +if [[ "$USE_LUKS" -eq 1 ]]; then + pass=${LUKS_PASSPHRASE:-} + if [[ -z "$pass" && $NON_INTERACTIVE -eq 0 ]]; then + pass=$(prompt_input "LUKS passphrase" "") + fi + if [[ -z "$pass" ]]; then + echo "LUKS passphrase required for encrypted install" + exit 1 + fi + printf "%s" "$pass" | cryptsetup luksFormat "${part_root}" - + printf "%s" "$pass" | cryptsetup open "${part_root}" cryptroot - + mapper_root="/dev/mapper/cryptroot" +else + mapper_root="${part_root}" +fi + +mkfs.btrfs -f "${mapper_root}" + +echo "[4/8] Creating Btrfs subvolumes" +mkdir -p /mnt +mount "${mapper_root}" /mnt +btrfs subvolume create /mnt/root +btrfs subvolume create /mnt/home +btrfs subvolume create /mnt/nix +umount /mnt + +echo "[5/8] Mounting subvolumes" +mount -o compress=zstd,subvol=root "${mapper_root}" /mnt +mkdir -p /mnt/{home,nix,boot} +mount -o compress=zstd,subvol=home "${mapper_root}" /mnt/home +mount -o compress=zstd,subvol=nix "${mapper_root}" /mnt/nix +mount "${part_boot}" /mnt/boot + +if [[ "${SWAP_GB}" -gt 0 ]]; then + echo "[5b/8] Creating swapfile (${SWAP_GB}G)" + mkdir -p /mnt/swap + chattr +C /mnt/swap || true + fallocate -l "${SWAP_GB}G" /mnt/swap/swapfile + chmod 600 /mnt/swap/swapfile + mkswap /mnt/swap/swapfile + swapon /mnt/swap/swapfile +fi + +echo "[6/8] Copying flake to /etc/nixos" +if command -v rsync >/dev/null 2>&1; then + rsync -a --exclude '.git' --exclude 'result' --exclude 'node_modules' "${REPO_ROOT}/" /mnt/etc/nixos/ +else + (cd "${REPO_ROOT}" && tar -cf - --exclude=.git --exclude=result --exclude=node_modules .) | (cd /mnt/etc/nixos && tar -xf -) +fi + +echo "[7/8] Generating hardware config" +hardware_file="/mnt/etc/nixos/hosts/${HOSTNAME}-hardware.nix" +mkdir -p /mnt/etc/nixos/hosts +nixos-generate-config --root /mnt --show-hardware-config > "${hardware_file}" + +if [[ "$USE_LUKS" -eq 1 || "${SWAP_GB}" -gt 0 ]]; then + tmpfile=$(mktemp) + # drop trailing closing brace + sed '$d' "${hardware_file}" > "${tmpfile}" + if [[ "$USE_LUKS" -eq 1 ]]; then + echo " boot.initrd.luks.devices.cryptroot.device = \"${part_root}\";" >> "${tmpfile}" + fi + if [[ "${SWAP_GB}" -gt 0 ]]; then + echo " swapDevices = [ { device = \"/swap/swapfile\"; } ];" >> "${tmpfile}" + fi + echo "}" >> "${tmpfile}" + mv "${tmpfile}" "${hardware_file}" +fi + +host_file="/mnt/etc/nixos/hosts/${HOSTNAME}.nix" +if [ ! -e "${host_file}" ]; then + cat > "${host_file}" <