feat: nomarchy-install — guided live-ISO installer (disko + mkFlake)
Ported from the previous iteration's installer (3bdfc35), adapted to the
mkFlake world. gum TUI: disk pick, optional LUKS2, user/hostname/timezone,
DMI → nixos-hardware autodetection (hardware-db.sh). disko partitions
GPT + 1 GiB ESP + BTRFS subvolumes; the generated machine flake lands at
~user/.nomarchy (one mkFlake call, /etc/nixos symlinks to it) and
nixos-install makes it bootable (UEFI/systemd-boot, v1 single disk).
Offline by construction: the target flake.lock is composed from the rev
the ISO was built from (compose-lock.py), `nix flake archive` seeds the
target store, and the ISO pre-builds the template system + desktop.
First boot lands themed: the HM generation is pre-activated in chroot.
mkFlake grows two things for this: hardwareProfile now also takes a list
(autodetection emits several common-* modules), and installed systems get
the standalone home-manager CLI from the pinned input. Unattended mode
(NOMARCHY_UNATTENDED=1 + env) documented in docs/TESTING.md §4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
21
README.md
21
README.md
@@ -58,7 +58,9 @@ Flat on purpose. Two module trees, one options file each, no hidden layers.
|
||||
├── hosts/
|
||||
│ ├── default/ # reference machine (thin: boot, user, hostname)
|
||||
│ └── live.nix # bootable live ISO (try the distro, no install)
|
||||
├── pkgs/nomarchy-theme-sync/ # state writer + rebuild dispatcher (Python)
|
||||
├── pkgs/
|
||||
│ ├── nomarchy-theme-sync/ # state writer + rebuild dispatcher (Python)
|
||||
│ └── nomarchy-install/ # live-ISO installer (gum + disko + mkFlake)
|
||||
├── templates/downstream/ # `nix flake init -t` starter for users
|
||||
├── docs/TESTING.md # how to verify changes (incl. AI-agent rules)
|
||||
└── tools/ # maintainer-only
|
||||
@@ -86,6 +88,17 @@ the locked inputs into the ISO store — so theme switching (including the
|
||||
`home-manager switch` it triggers) works **offline**, exactly like on an
|
||||
installed system. Verification checklist: [docs/TESTING.md](docs/TESTING.md).
|
||||
|
||||
Like what you see? **`nomarchy-install`** (in a terminal) walks you through
|
||||
installing to disk: pick a disk, optional LUKS2 full-disk encryption, user +
|
||||
hostname + timezone, hardware autodetection (DMI → nixos-hardware profile),
|
||||
then disko partitions (GPT + ESP + BTRFS subvolumes) and `nixos-install`
|
||||
runs — **without a network** when the ISO was built from a clean tree (the
|
||||
target's `flake.lock` is composed from the rev the ISO carries). The
|
||||
installed machine gets the standard downstream layout: the flake at
|
||||
`~/.nomarchy` (`/etc/nixos` symlinks to it), one `mkFlake` call, your
|
||||
`system.nix`/`home.nix`. First boot lands in the fully themed desktop —
|
||||
the installer pre-activates the Home Manager generation. UEFI only for now.
|
||||
|
||||
## 3. Using Nomarchy on your machine (downstream)
|
||||
|
||||
Nomarchy is consumed as a flake input — you never fork or edit this repo:
|
||||
@@ -112,7 +125,7 @@ outputs = { nomarchy, ... }:
|
||||
|---|---|---|
|
||||
| `src` | — (required) | Your flake directory (`./.`) |
|
||||
| `username` | — (required) | Login name; flows into `system.nix` and names the HM config |
|
||||
| `hardwareProfile` | `null` | A [nixos-hardware](https://github.com/NixOS/nixos-hardware) module name (pinned + tested by Nomarchy; unknown names fail with suggestions) |
|
||||
| `hardwareProfile` | `null` | One [nixos-hardware](https://github.com/NixOS/nixos-hardware) module name, or a list of them (pinned + tested by Nomarchy; unknown names fail with suggestions) |
|
||||
| `system` | `"x86_64-linux"` | Platform |
|
||||
|
||||
(Power users can skip `mkFlake` and wire `nixosModules.nomarchy` /
|
||||
@@ -216,6 +229,6 @@ workspaces · `Print` region screenshot.
|
||||
re-copy on every state write is the main eval tax), then pre-built theme
|
||||
variants if still needed
|
||||
- Plymouth + SDDM/greeter theming from the same JSON
|
||||
- Interactive installer on the live ISO (disko BTRFS+LUKS, DMI →
|
||||
`hardwareProfile` autodetection feeding `mkFlake`) — port from old_distro
|
||||
- Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy
|
||||
boot (v1 `nomarchy-install` is single-disk UEFI — see `pkgs/nomarchy-install`)
|
||||
- `hyprlock`/`hypridle`, swayosd, launch-or-focus UX scripts
|
||||
|
||||
@@ -67,7 +67,27 @@ Work through these in order; each one exercises a different layer.
|
||||
Items 6 and 8 are the big ones — they prove the all-Home-Manager theming
|
||||
model end to end.
|
||||
|
||||
## 4. VM-specific gotchas
|
||||
## 4. Testing the installer
|
||||
|
||||
The installer has an unattended mode for exactly this. Boot the live ISO in
|
||||
QEMU **with a second, blank disk attached** (e.g. `-drive
|
||||
file=target.img,if=virtio,format=raw` on a 20 G image), then in the live
|
||||
terminal:
|
||||
|
||||
```sh
|
||||
sudo NOMARCHY_UNATTENDED=1 NOMARCHY_DISK=/dev/vda \
|
||||
NOMARCHY_USERNAME=me NOMARCHY_PASSWORD=test \
|
||||
NOMARCHY_FINISH=poweroff nomarchy-install
|
||||
```
|
||||
|
||||
Then boot the VM again **from the target disk** (drop `-cdrom`). The
|
||||
machine must reach the themed desktop on its own — greeter autostart,
|
||||
wallpaper, Waybar — and `nomarchy-theme-sync apply <x>` must work. For the
|
||||
offline claim, add `restrict=on` to the `-netdev` and re-run the whole
|
||||
flow: it must behave identically (requires an ISO built from a clean git
|
||||
tree, which is what bakes the rev into the installer).
|
||||
|
||||
## 5. VM-specific gotchas
|
||||
|
||||
- **No KVM** (e.g. nested without acceleration): everything works but
|
||||
`home-manager switch` will be painfully slow. Don't read slowness as
|
||||
@@ -79,7 +99,7 @@ model end to end.
|
||||
- **First theme switch is the slowest**: it evaluates the flake on a RAM
|
||||
disk. Subsequent switches reuse the eval cache.
|
||||
|
||||
## 5. When something fails
|
||||
## 6. When something fails
|
||||
|
||||
- Session/login problems: `journalctl -b -u greetd`, then `journalctl
|
||||
--user -b`.
|
||||
|
||||
42
flake.nix
42
flake.nix
@@ -33,6 +33,28 @@
|
||||
inherit system;
|
||||
overlays = [ self.overlays.default ];
|
||||
};
|
||||
|
||||
# The guided installer — live-ISO only (not in the overlay): it bakes
|
||||
# in the downstream template, this checkout's flake.lock, and the
|
||||
# source identity of this very revision so installs work offline.
|
||||
gitUrl = "https://git.bemagri.xyz/bernardo/nomarchy.git";
|
||||
nomarchyInstall = pkgs.callPackage ./pkgs/nomarchy-install {
|
||||
templateDir = ./templates/downstream;
|
||||
nomarchyLock = ./flake.lock;
|
||||
hardwareModuleNames = pkgs.writeText "nixos-hardware-modules.txt"
|
||||
(nixpkgs.lib.concatStringsSep "\n"
|
||||
(builtins.attrNames nixos-hardware.nixosModules));
|
||||
flakeUrl = "git+${gitUrl}";
|
||||
originalNode = { type = "git"; url = gitUrl; };
|
||||
lockedNode = {
|
||||
type = "git";
|
||||
url = gitUrl;
|
||||
rev = self.rev or null; # null on a dirty tree → installer needs network
|
||||
narHash = self.narHash;
|
||||
lastModified = self.lastModified or 0;
|
||||
revCount = self.revCount or 0;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
# ─── Downstream API ────────────────────────────────────────────
|
||||
@@ -75,6 +97,7 @@
|
||||
|
||||
packages.${system} = {
|
||||
nomarchy-theme-sync = pkgs.nomarchy-theme-sync;
|
||||
nomarchy-install = nomarchyInstall;
|
||||
default = pkgs.nomarchy-theme-sync;
|
||||
};
|
||||
|
||||
@@ -163,9 +186,11 @@
|
||||
};
|
||||
|
||||
# The standalone CLI that `nomarchy-theme-sync apply` runs for
|
||||
# theme switching, from the same pinned input.
|
||||
# theme switching, from the same pinned input — plus the
|
||||
# guided installer.
|
||||
environment.systemPackages = [
|
||||
home-manager.packages.${system}.home-manager
|
||||
nomarchyInstall
|
||||
];
|
||||
|
||||
# Keep the locked flake inputs in the ISO store so theme
|
||||
@@ -179,7 +204,20 @@
|
||||
[ input.outPath ] ++ builtins.concatMap collectInputs
|
||||
(builtins.attrValues (input.inputs or { }));
|
||||
in
|
||||
collectInputs self;
|
||||
collectInputs self ++ (
|
||||
# Pre-build the downstream template's system + desktop so
|
||||
# nomarchy-install can build the target offline (and fast):
|
||||
# a custom username/hostname changes only a handful of
|
||||
# derivations on top of these. No hardware profile — that
|
||||
# matches a VM install; profiled installs share most of it.
|
||||
let template = self.lib.mkFlake {
|
||||
src = ./templates/downstream;
|
||||
username = username;
|
||||
}; in [
|
||||
template.nixosConfigurations.default.config.system.build.toplevel
|
||||
template.homeConfigurations.${username}.activationPackage
|
||||
]
|
||||
);
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
The graphical session autologins as '${username}' (no password).
|
||||
Theme switching: nomarchy-theme-sync apply <name> (or SUPER+T)
|
||||
Wallpapers: nomarchy-theme-sync bg next (or SUPER+SHIFT+T)
|
||||
Install to disk: nomarchy-install
|
||||
The flake lives at ~/.nomarchy.
|
||||
'';
|
||||
|
||||
@@ -76,7 +77,7 @@
|
||||
# Welcome toast once the session is up (concatenated onto the
|
||||
# base exec-once list).
|
||||
exec-once = [
|
||||
"sh -c 'sleep 3; notify-send -a Nomarchy \"Welcome to Nomarchy\" \"SUPER+Return terminal · SUPER+T themes · SUPER+SHIFT+T wallpaper\"'"
|
||||
"sh -c 'sleep 3; notify-send -a Nomarchy \"Welcome to Nomarchy\" \"SUPER+Return terminal · SUPER+T themes · install with nomarchy-install\"'"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
31
lib.nix
31
lib.nix
@@ -9,23 +9,28 @@
|
||||
mkFlake =
|
||||
{ src # the downstream flake directory (./.)
|
||||
, username # login name; also the homeConfigurations attr
|
||||
, hardwareProfile ? null # a nixos-hardware module name, or null
|
||||
, hardwareProfile ? null # nixos-hardware module name(s): string, list, or null
|
||||
, system ? "x86_64-linux"
|
||||
}:
|
||||
let
|
||||
inherit (nixpkgs) lib;
|
||||
|
||||
hardwareModules =
|
||||
if hardwareProfile == null then
|
||||
[ ]
|
||||
else if builtins.hasAttr hardwareProfile nixos-hardware.nixosModules then
|
||||
[ nixos-hardware.nixosModules.${hardwareProfile} ]
|
||||
# One profile or several (the installer's autodetection emits a few
|
||||
# common-* modules alongside the model-specific one).
|
||||
profileNames =
|
||||
if hardwareProfile == null then [ ]
|
||||
else if builtins.isList hardwareProfile then hardwareProfile
|
||||
else [ hardwareProfile ];
|
||||
|
||||
lookupProfile = name:
|
||||
if builtins.hasAttr name nixos-hardware.nixosModules then
|
||||
nixos-hardware.nixosModules.${name}
|
||||
else
|
||||
throw ''
|
||||
nomarchy.lib.mkFlake: unknown hardwareProfile "${hardwareProfile}".
|
||||
nomarchy.lib.mkFlake: unknown hardwareProfile "${name}".
|
||||
${let
|
||||
close = builtins.filter
|
||||
(name: lib.strings.levenshteinAtMost 3 name hardwareProfile)
|
||||
(candidate: lib.strings.levenshteinAtMost 3 candidate name)
|
||||
(builtins.attrNames nixos-hardware.nixosModules);
|
||||
in
|
||||
lib.optionalString (close != [ ])
|
||||
@@ -37,6 +42,8 @@
|
||||
Omit hardwareProfile entirely if your machine has no profile.
|
||||
'';
|
||||
|
||||
hardwareModules = map lookupProfile profileNames;
|
||||
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ nomarchy.overlays.default ];
|
||||
@@ -49,7 +56,13 @@
|
||||
inherit system;
|
||||
specialArgs = { inherit username; };
|
||||
modules =
|
||||
[ nomarchy.nixosModules.nomarchy ]
|
||||
[
|
||||
nomarchy.nixosModules.nomarchy
|
||||
# The standalone HM CLI ships with the system so theme switching
|
||||
# (`nomarchy-theme-sync apply` → `home-manager switch`) works
|
||||
# out of the box — same pinned input as the desktop modules.
|
||||
{ environment.systemPackages = [ home-manager.packages.${system}.home-manager ]; }
|
||||
]
|
||||
++ hardwareModules
|
||||
++ [
|
||||
(src + "/hardware-configuration.nix") # from nixos-generate-config
|
||||
|
||||
57
pkgs/nomarchy-install/compose-lock.py
Normal file
57
pkgs/nomarchy-install/compose-lock.py
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compose a flake.lock for the freshly installed machine — offline.
|
||||
|
||||
The generated downstream flake has exactly one input, `nomarchy`. A normal
|
||||
`nix flake lock` would fetch it from its forge; on an offline install we
|
||||
already KNOW the answer: the live ISO was built from a specific nomarchy
|
||||
rev whose source (and all transitive inputs, via the ISO's pinned store
|
||||
paths) is on hand. So we write the lock nix would have written:
|
||||
|
||||
root ─▶ nomarchy (locked to the baked rev+narHash)
|
||||
└─▶ every node from nomarchy's own flake.lock, verbatim
|
||||
|
||||
Nix resolves locked inputs by narHash against the store before touching
|
||||
the network, so evaluation on the target works without connectivity.
|
||||
|
||||
Usage:
|
||||
compose-lock.py <nomarchy-flake.lock> <out> <locked-json> <original-json>
|
||||
|
||||
where <locked-json>/<original-json> are the flake-lock node fields for the
|
||||
nomarchy input (any input type — github, git, …), baked at package build.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> None:
|
||||
src_lock, out, locked_json, original_json = sys.argv[1:5]
|
||||
|
||||
locked = json.loads(locked_json)
|
||||
if not locked.get("rev"):
|
||||
sys.exit("compose-lock: no rev in locked metadata (ISO built from a dirty tree?)")
|
||||
|
||||
with open(src_lock) as f:
|
||||
upstream = json.load(f)
|
||||
|
||||
nodes = dict(upstream["nodes"])
|
||||
# nomarchy's root node describes ITS inputs; that mapping becomes the
|
||||
# input set of the new `nomarchy` node.
|
||||
nomarchy_inputs = nodes.pop("root")["inputs"]
|
||||
if "nomarchy" in nodes:
|
||||
sys.exit("compose-lock: upstream lock already has a 'nomarchy' node")
|
||||
|
||||
nodes["nomarchy"] = {
|
||||
"inputs": nomarchy_inputs,
|
||||
"locked": locked,
|
||||
"original": json.loads(original_json),
|
||||
}
|
||||
nodes["root"] = {"inputs": {"nomarchy": "nomarchy"}}
|
||||
|
||||
with open(out, "w") as f:
|
||||
json.dump({"nodes": nodes, "root": "root", "version": 7}, f, indent=2)
|
||||
f.write("\n")
|
||||
print(f"compose-lock: wrote {out} (nomarchy @ {locked['rev'][:12]})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
71
pkgs/nomarchy-install/default.nix
Normal file
71
pkgs/nomarchy-install/default.nix
Normal file
@@ -0,0 +1,71 @@
|
||||
{ lib
|
||||
, stdenvNoCC
|
||||
, makeWrapper
|
||||
, bash
|
||||
, gum
|
||||
, disko
|
||||
, whois # mkpasswd
|
||||
, git
|
||||
, python3
|
||||
, util-linux # lsblk, wipefs, swapoff, lscpu
|
||||
, gptfdisk # sgdisk
|
||||
, parted # partprobe
|
||||
, cryptsetup
|
||||
, lvm2 # dmsetup
|
||||
, pciutils # lspci
|
||||
# Baked metadata — what this installer installs and where it came from.
|
||||
, templateDir # templates/downstream (home.nix, theme-state.json)
|
||||
, nomarchyLock # the distro's flake.lock (for offline lock composition)
|
||||
, hardwareModuleNames # newline-separated nixos-hardware module names
|
||||
, flakeUrl # flake-style URL written into the generated flake.nix
|
||||
, lockedNode # attrset: the flake.lock "locked" node for nomarchy
|
||||
# (rev = null when the ISO was built from a dirty tree)
|
||||
, originalNode # attrset: the flake.lock "original" node
|
||||
}:
|
||||
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "nomarchy-install";
|
||||
version = "0.1.0";
|
||||
|
||||
src = ./.;
|
||||
|
||||
nativeBuildInputs = [ makeWrapper ];
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
install -Dm755 nomarchy-install.sh $out/bin/nomarchy-install
|
||||
patchShebangs $out/bin/nomarchy-install
|
||||
|
||||
share=$out/share/nomarchy-install
|
||||
install -Dm644 disko-config.nix "$share/disko-config.nix"
|
||||
install -Dm644 hardware-db.sh "$share/hardware-db.sh"
|
||||
install -Dm644 compose-lock.py "$share/compose-lock.py"
|
||||
install -Dm644 ${nomarchyLock} "$share/flake.lock"
|
||||
install -Dm644 ${hardwareModuleNames} "$share/hardware-modules.txt"
|
||||
mkdir -p "$share/template"
|
||||
cp ${templateDir}/home.nix ${templateDir}/theme-state.json "$share/template/"
|
||||
|
||||
# nixos-install / nixos-generate-config / nixos-enter / nix / systemd
|
||||
# tools come from the live system on purpose — they must match it.
|
||||
wrapProgram $out/bin/nomarchy-install \
|
||||
--prefix PATH : ${lib.makeBinPath [
|
||||
bash gum disko whois git python3
|
||||
util-linux gptfdisk parted cryptsetup lvm2 pciutils
|
||||
]} \
|
||||
--set NOMARCHY_INSTALL_SHARE "$share" \
|
||||
--set NOMARCHY_FLAKE_URL ${lib.escapeShellArg flakeUrl} \
|
||||
--set NOMARCHY_REV ${lib.escapeShellArg (toString (lockedNode.rev or ""))} \
|
||||
--set NOMARCHY_LOCKED_JSON ${lib.escapeShellArg (builtins.toJSON lockedNode)} \
|
||||
--set NOMARCHY_ORIGINAL_JSON ${lib.escapeShellArg (builtins.toJSON originalNode)}
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Nomarchy guided installer (disko + mkFlake + nixos-install)";
|
||||
license = lib.licenses.mit;
|
||||
mainProgram = "nomarchy-install";
|
||||
platforms = lib.platforms.linux;
|
||||
};
|
||||
}
|
||||
70
pkgs/nomarchy-install/disko-config.nix
Normal file
70
pkgs/nomarchy-install/disko-config.nix
Normal file
@@ -0,0 +1,70 @@
|
||||
# Nomarchy installer disk layout — single disk, GPT, 1 GiB ESP, BTRFS
|
||||
# root with subvolumes, optionally under LUKS2. Invoked by
|
||||
# nomarchy-install via:
|
||||
#
|
||||
# disko --mode destroy,format,mount \
|
||||
# --argstr mainDrive /dev/nvme0n1 \
|
||||
# --arg withLuks true \
|
||||
# disko-config.nix
|
||||
#
|
||||
# Ported from the previous iteration's golden-path config (multi-disk
|
||||
# RAID and the impermanence snapshot dropped for v1 — see git history
|
||||
# of installer/disko-config.nix at 3bdfc35 when adding them back).
|
||||
# Function arguments, not sed-templating: escaping shell into Nix
|
||||
# proved fragile twice before.
|
||||
{ mainDrive
|
||||
, withLuks ? true
|
||||
, ...
|
||||
}:
|
||||
|
||||
let
|
||||
btrfsMountOptions = [ "compress=zstd" "noatime" ];
|
||||
|
||||
rootBtrfs = {
|
||||
type = "btrfs";
|
||||
extraArgs = [ "-f" ];
|
||||
subvolumes = {
|
||||
"@" = { mountpoint = "/"; mountOptions = btrfsMountOptions; };
|
||||
"@home" = { mountpoint = "/home"; mountOptions = btrfsMountOptions; };
|
||||
"@nix" = { mountpoint = "/nix"; mountOptions = btrfsMountOptions; };
|
||||
"@log" = { mountpoint = "/var/log"; mountOptions = btrfsMountOptions; };
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
disko.devices.disk.main = {
|
||||
type = "disk";
|
||||
device = mainDrive;
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
# 1 GiB ESP — fits several kernel generations + initrd.
|
||||
ESP = {
|
||||
priority = 1;
|
||||
name = "ESP";
|
||||
start = "1M";
|
||||
end = "1G";
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
mountOptions = [ "umask=0077" ];
|
||||
};
|
||||
};
|
||||
root = {
|
||||
size = "100%";
|
||||
content =
|
||||
if withLuks then {
|
||||
type = "luks";
|
||||
name = "crypted";
|
||||
passwordFile = "/tmp/nomarchy-luks.key";
|
||||
settings.allowDiscards = true;
|
||||
content = rootBtrfs;
|
||||
} else
|
||||
rootBtrfs;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
177
pkgs/nomarchy-install/hardware-db.sh
Normal file
177
pkgs/nomarchy-install/hardware-db.sh
Normal file
@@ -0,0 +1,177 @@
|
||||
# Nomarchy hardware database — sourced by nomarchy-install.
|
||||
#
|
||||
# Flat table mapping detected DMI fields to nixos-hardware module names
|
||||
# (the strings nomarchy.lib.mkFlake's hardwareProfile accepts). Ported
|
||||
# from the previous iteration (installer/hardware-db.sh at 3bdfc35).
|
||||
#
|
||||
# Format (pipe-separated):
|
||||
# sys_vendor_regex | product_regex | nixos_hardware_module
|
||||
#
|
||||
# - Matching is bash `[[ =~ ]]` regex, case-insensitive.
|
||||
# - First match wins — put more specific entries (e.g. exact model years)
|
||||
# above broader fallbacks.
|
||||
#
|
||||
# To add a new device: grab `/sys/class/dmi/id/product_name` on the
|
||||
# target machine and add a line. Module names must exist in
|
||||
# nixos-hardware.nixosModules (mkFlake fails with suggestions if not).
|
||||
|
||||
HARDWARE_DB=(
|
||||
# Framework ---------------------------------------------------------------
|
||||
# Module names follow nixos-hardware's actual attrs — for Framework 13
|
||||
# the per-generation modules dropped the "13-" prefix.
|
||||
"Framework|Laptop 16.*AMD|framework-16-7040-amd"
|
||||
"Framework|Laptop 16.*Ryzen AI 300|framework-16-amd-ai-300-series"
|
||||
"Framework|Laptop 13.*Ryzen AI 300|framework-amd-ai-300-series"
|
||||
"Framework|Laptop 13.*Ryzen 7040|framework-13-7040-amd"
|
||||
"Framework|Laptop 13.*Core Ultra|framework-intel-core-ultra-series1"
|
||||
"Framework|Laptop 13.*13th Gen Intel|framework-13th-gen-intel"
|
||||
"Framework|Laptop 13.*12th Gen Intel|framework-12th-gen-intel"
|
||||
"Framework|Laptop 13.*11th Gen Intel|framework-11th-gen-intel"
|
||||
"Framework|Laptop \(13.*|framework"
|
||||
|
||||
# Dell XPS / Precision / Latitude ----------------------------------------
|
||||
"Dell|XPS 15 9500|dell-xps-15-9500"
|
||||
"Dell|XPS 15 9510|dell-xps-15-9510"
|
||||
"Dell|XPS 15 9520|dell-xps-15-9520"
|
||||
"Dell|XPS 13 9310|dell-xps-13-9310"
|
||||
"Dell|XPS 13 9370|dell-xps-13-9370"
|
||||
"Dell|XPS 13 9380|dell-xps-13-9380"
|
||||
"Dell|XPS 13 7390|dell-xps-13-7390"
|
||||
"Dell|Precision 5530|dell-precision-5530"
|
||||
"Dell|Latitude 7490|dell-latitude-7490"
|
||||
"Dell|Latitude 7430|dell-latitude-7430"
|
||||
"Dell|Latitude 7420|dell-latitude-7420"
|
||||
|
||||
# Lenovo ThinkPad --------------------------------------------------------
|
||||
# X1 Carbon: the per-gen modules are named "x1-Nth-gen", not
|
||||
# "x1-carbon-genN" — the X1 series IS the Carbon line.
|
||||
"LENOVO|ThinkPad X1 Carbon Gen 11|lenovo-thinkpad-x1-11th-gen"
|
||||
"LENOVO|ThinkPad X1 Carbon Gen 10|lenovo-thinkpad-x1-10th-gen"
|
||||
"LENOVO|ThinkPad X1 Carbon Gen 9|lenovo-thinkpad-x1-9th-gen"
|
||||
"LENOVO|ThinkPad X1 Carbon Gen 7|lenovo-thinkpad-x1-7th-gen"
|
||||
"LENOVO|ThinkPad X1 Carbon Gen 6|lenovo-thinkpad-x1-6th-gen"
|
||||
"LENOVO|ThinkPad X1 Extreme|lenovo-thinkpad-x1-extreme"
|
||||
"LENOVO|ThinkPad X1 Nano|lenovo-thinkpad-x1-nano-gen1"
|
||||
"LENOVO|ThinkPad T14 Gen 3|lenovo-thinkpad-t14-amd-gen3"
|
||||
"LENOVO|ThinkPad T14 Gen 2|lenovo-thinkpad-t14-amd-gen2"
|
||||
"LENOVO|ThinkPad T14 Gen 1|lenovo-thinkpad-t14-amd-gen1"
|
||||
"LENOVO|ThinkPad T480|lenovo-thinkpad-t480"
|
||||
"LENOVO|ThinkPad L13|lenovo-thinkpad-l13"
|
||||
"LENOVO|ThinkPad P14s.*Gen 5|lenovo-thinkpad-p14s-amd-gen5"
|
||||
"LENOVO|ThinkPad P14s.*Gen 4|lenovo-thinkpad-p14s-amd-gen4"
|
||||
"LENOVO|ThinkPad P14s.*Gen 3|lenovo-thinkpad-p14s-amd-gen3"
|
||||
|
||||
# Microsoft Surface ------------------------------------------------------
|
||||
# nixos-hardware ships per-chip modules, not per-revision.
|
||||
"Microsoft|Surface Pro 10|microsoft-surface-pro-intel"
|
||||
"Microsoft|Surface Pro 9|microsoft-surface-pro-9"
|
||||
"Microsoft|Surface Pro 8|microsoft-surface-pro-intel"
|
||||
"Microsoft|Surface Pro 7|microsoft-surface-pro-intel"
|
||||
"Microsoft|Surface Pro 6|microsoft-surface-pro-intel"
|
||||
"Microsoft|Surface Pro 3|microsoft-surface-pro-3"
|
||||
"Microsoft|Surface Laptop.*Ryzen|microsoft-surface-laptop-amd"
|
||||
"Microsoft|Surface Go|microsoft-surface-go"
|
||||
|
||||
# ASUS ROG Ally / Zephyrus / Strix ---------------------------------------
|
||||
"ASUS.*|RC71L|asus-ally-rc71l"
|
||||
"ASUS.*|RC72LA|asus-ally-rc71l"
|
||||
"ASUS.*|ROG Ally|asus-ally-rc71l"
|
||||
"ASUS.*|ROG Zephyrus G14.*GA402X|asus-zephyrus-ga402x"
|
||||
"ASUS.*|ROG Zephyrus G14.*GA402|asus-zephyrus-ga402"
|
||||
"ASUS.*|ROG Zephyrus G14.*GA401|asus-zephyrus-ga401"
|
||||
"ASUS.*|ROG Zephyrus G15|asus-zephyrus-ga503"
|
||||
"ASUS.*|ROG Zephyrus.*GU603|asus-zephyrus-gu603h"
|
||||
"ASUS.*|ROG Strix G513|asus-rog-strix-g513im"
|
||||
"ASUS.*|ROG Strix G533|asus-rog-strix-g533zw"
|
||||
|
||||
# Apple (T2 Intel; M-series is aarch64 — out of scope) -------------------
|
||||
"Apple.*|MacBookPro15|apple-t2"
|
||||
"Apple.*|MacBookPro16|apple-t2"
|
||||
"Apple.*|MacBookAir8|apple-t2"
|
||||
"Apple.*|MacBookAir9|apple-t2"
|
||||
"Apple.*|iMac19|apple-t2"
|
||||
"Apple.*|iMacPro1|apple-t2"
|
||||
|
||||
# System76 ---------------------------------------------------------------
|
||||
"System76|Oryx Pro.*|system76"
|
||||
"System76|Lemur Pro.*|system76"
|
||||
"System76|Darter Pro.*|system76"
|
||||
"System76|Galago Pro.*|system76"
|
||||
"System76|Pangolin.*|system76"
|
||||
|
||||
# Known-unsupported (so contributors know they're not missing):
|
||||
# - Valve Steam Deck: Jovian-NixOS territory, not nixos-hardware.
|
||||
# - Snapdragon X laptops / Raspberry Pi: aarch64; installer is x86_64.
|
||||
)
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# nomarchy_detect_hw — inspects DMI + lspci + /sys, emits one line per hit:
|
||||
# MODULE <nixos-hardware module name>
|
||||
# DETAIL <human-readable explanation>
|
||||
# Returns 0 if at least one module was detected.
|
||||
# ----------------------------------------------------------------------------
|
||||
nomarchy_detect_hw() {
|
||||
local sys_vendor product_name cpu_vendor
|
||||
sys_vendor=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "")
|
||||
product_name=$(cat /sys/class/dmi/id/product_name 2>/dev/null || echo "")
|
||||
cpu_vendor=$(lscpu 2>/dev/null | awk -F: '/Vendor ID/{gsub(/ /,"",$2); print $2; exit}')
|
||||
|
||||
local detected=0
|
||||
|
||||
# CPU
|
||||
case "$cpu_vendor" in
|
||||
AuthenticAMD) echo "MODULE common-cpu-amd"; echo "DETAIL cpu: AMD"; detected=1 ;;
|
||||
GenuineIntel) echo "MODULE common-cpu-intel"; echo "DETAIL cpu: Intel"; detected=1 ;;
|
||||
esac
|
||||
|
||||
# GPU (lspci may list several; report all)
|
||||
if command -v lspci >/dev/null 2>&1; then
|
||||
local gpu_line nvidia=0 amdgpu=0 intelgpu=0
|
||||
while IFS= read -r gpu_line; do
|
||||
case "$gpu_line" in
|
||||
*"[10de:"*|*"NVIDIA"*) nvidia=1 ;;
|
||||
*"[1002:"*|*"AMD/ATI"*|*"Advanced Micro Devices"*) amdgpu=1 ;;
|
||||
*"[8086:"*|*"Intel Corporation"*) intelgpu=1 ;;
|
||||
esac
|
||||
done < <(lspci -nn 2>/dev/null | grep -iE 'vga|3d|display')
|
||||
|
||||
(( nvidia )) && { echo "MODULE common-gpu-nvidia"; echo "DETAIL gpu: NVIDIA"; detected=1; }
|
||||
(( amdgpu )) && { echo "MODULE common-gpu-amd"; echo "DETAIL gpu: AMD"; detected=1; }
|
||||
(( intelgpu )) && { echo "MODULE common-gpu-intel"; echo "DETAIL gpu: Intel"; detected=1; }
|
||||
fi
|
||||
|
||||
# Chassis
|
||||
if compgen -G "/sys/class/power_supply/BAT*" >/dev/null; then
|
||||
echo "MODULE common-pc-laptop"
|
||||
echo "DETAIL chassis: laptop (battery present)"
|
||||
else
|
||||
echo "MODULE common-pc"
|
||||
echo "DETAIL chassis: desktop"
|
||||
fi
|
||||
detected=1
|
||||
|
||||
# SSD
|
||||
local rotational
|
||||
rotational=$(cat /sys/block/nvme0n1/queue/rotational 2>/dev/null \
|
||||
|| cat /sys/block/sda/queue/rotational 2>/dev/null || echo "")
|
||||
if [[ "$rotational" == "0" ]]; then
|
||||
echo "MODULE common-pc-ssd"
|
||||
echo "DETAIL storage: SSD"
|
||||
fi
|
||||
|
||||
# Model-specific match from the DB
|
||||
local entry v_re p_re mod
|
||||
for entry in "${HARDWARE_DB[@]}"; do
|
||||
IFS='|' read -r v_re p_re mod <<< "$entry"
|
||||
shopt -s nocasematch
|
||||
if [[ "$sys_vendor" =~ $v_re ]] && [[ "$product_name" =~ $p_re ]]; then
|
||||
shopt -u nocasematch
|
||||
echo "MODULE $mod"
|
||||
echo "DETAIL model: $sys_vendor $product_name → $mod"
|
||||
break
|
||||
fi
|
||||
shopt -u nocasematch
|
||||
done
|
||||
|
||||
return $(( 1 - detected ))
|
||||
}
|
||||
384
pkgs/nomarchy-install/nomarchy-install.sh
Normal file
384
pkgs/nomarchy-install/nomarchy-install.sh
Normal file
@@ -0,0 +1,384 @@
|
||||
#!/usr/bin/env bash
|
||||
# nomarchy-install — guided installer for the Nomarchy live ISO.
|
||||
#
|
||||
# Partitions one disk (GPT + 1 GiB ESP + BTRFS, optional LUKS2) with disko,
|
||||
# generates a downstream machine flake at /home/<user>/.nomarchy (one
|
||||
# nomarchy.lib.mkFlake call — the flake the user never hand-edits), and
|
||||
# runs nixos-install. Works offline: the ISO pins every flake input, and
|
||||
# the target flake.lock is composed from the rev the ISO was built from.
|
||||
#
|
||||
# Ported from the previous iteration's installer/install.sh (3bdfc35) —
|
||||
# the pre-wipe and ordering comments carry its hard-won fixes. v1 scope:
|
||||
# single disk, UEFI only. Multi-disk RAID and impermanence live in git
|
||||
# history when they're wanted back.
|
||||
#
|
||||
# Unattended mode (for CI/VM tests):
|
||||
# NOMARCHY_UNATTENDED=1 NOMARCHY_DISK=/dev/vda NOMARCHY_USERNAME=me \
|
||||
# NOMARCHY_PASSWORD=secret [NOMARCHY_HOSTNAME=nomarchy] \
|
||||
# [NOMARCHY_TIMEZONE=UTC] [NOMARCHY_LUKS_PASSPHRASE=...] \
|
||||
# [NOMARCHY_HW="auto"|"none"|"mod1 mod2"] [NOMARCHY_FINISH=none|reboot|poweroff]
|
||||
# nomarchy-install
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Baked in by the package wrapper:
|
||||
# NOMARCHY_INSTALL_SHARE — disko-config.nix, hardware-db.sh,
|
||||
# compose-lock.py, flake.lock, template/,
|
||||
# hardware-modules.txt
|
||||
# NOMARCHY_FLAKE_URL — flake URL written into the generated flake.nix
|
||||
# NOMARCHY_REV — nomarchy rev this ISO was built from ("" if dirty)
|
||||
# NOMARCHY_LOCKED_JSON / NOMARCHY_ORIGINAL_JSON — flake.lock node fields
|
||||
SHARE="${NOMARCHY_INSTALL_SHARE:?not run via the packaged wrapper}"
|
||||
|
||||
UNATTENDED="${NOMARCHY_UNATTENDED:-0}"
|
||||
LUKS_KEY_PATH="/tmp/nomarchy-luks.key"
|
||||
|
||||
# ─── UI helpers ─────────────────────────────────────────────────────────
|
||||
header() { gum style --border rounded --padding "0 2" --margin "1 0" \
|
||||
--border-foreground 212 "$@"; }
|
||||
section() { gum style --foreground 212 --bold "── $* ──"; }
|
||||
info() { gum style --foreground 245 " $*"; }
|
||||
success() { gum style --foreground 42 " ✓ $*"; }
|
||||
warn() { gum style --foreground 214 " ⚠ $*"; }
|
||||
fail() { gum style --foreground 9 " ✗ $*"; exit 1; }
|
||||
|
||||
confirm() { # confirm <prompt> — auto-yes when unattended
|
||||
[[ "$UNATTENDED" == "1" ]] && return 0
|
||||
gum confirm "$1"
|
||||
}
|
||||
|
||||
# ─── Environment checks ─────────────────────────────────────────────────
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
exec sudo --preserve-env "$0" "$@"
|
||||
fi
|
||||
|
||||
header "Nomarchy installer" "NixOS, themed and ready to go."
|
||||
|
||||
[[ -d /sys/firmware/efi ]] \
|
||||
|| fail "No UEFI firmware detected. v1 installs systemd-boot and needs UEFI (BIOS/legacy: see roadmap)."
|
||||
command -v nixos-install >/dev/null \
|
||||
|| fail "nixos-install not found — run this from the Nomarchy live ISO."
|
||||
grep -q nomarchy-live /etc/hostname 2>/dev/null \
|
||||
|| warn "This doesn't look like the Nomarchy live ISO; proceeding anyway."
|
||||
|
||||
# ─── Disk selection ─────────────────────────────────────────────────────
|
||||
section "Target disk"
|
||||
|
||||
# Never offer the medium we're running from.
|
||||
live_disk=""
|
||||
live_src=$(findmnt -no SOURCE /iso 2>/dev/null || true)
|
||||
[[ -n "$live_src" ]] && live_disk=$(lsblk -no PKNAME "$live_src" 2>/dev/null || true)
|
||||
|
||||
mapfile -t disks < <(lsblk -dpno NAME,SIZE,MODEL,TYPE \
|
||||
| awk -v skip="/dev/${live_disk:-NONE}" \
|
||||
'$NF == "disk" && $1 != skip && $1 !~ /loop|zram|sr[0-9]/ {NF--; print}')
|
||||
[[ ${#disks[@]} -gt 0 ]] || fail "No installable disks found."
|
||||
|
||||
if [[ "$UNATTENDED" == "1" ]]; then
|
||||
TARGET_DISK="${NOMARCHY_DISK:?NOMARCHY_DISK required in unattended mode}"
|
||||
else
|
||||
choice=$(printf '%s\n' "${disks[@]}" \
|
||||
| gum choose --header "Install Nomarchy on which disk? (EVERYTHING on it will be erased)")
|
||||
TARGET_DISK="${choice%% *}"
|
||||
fi
|
||||
[[ -b "$TARGET_DISK" ]] || fail "$TARGET_DISK is not a block device."
|
||||
info "Target: $TARGET_DISK"
|
||||
|
||||
# ─── Encryption ─────────────────────────────────────────────────────────
|
||||
section "Disk encryption"
|
||||
|
||||
LUKS_PASSPHRASE=""
|
||||
if [[ "$UNATTENDED" == "1" ]]; then
|
||||
LUKS_PASSPHRASE="${NOMARCHY_LUKS_PASSPHRASE:-}"
|
||||
elif gum confirm "Encrypt the disk with LUKS? (recommended for laptops)"; then
|
||||
while true; do
|
||||
p1=$(gum input --password --placeholder "LUKS passphrase (min 8 chars)")
|
||||
[[ ${#p1} -ge 8 ]] || { warn "Too short."; continue; }
|
||||
p2=$(gum input --password --placeholder "Repeat passphrase")
|
||||
[[ "$p1" == "$p2" ]] && { LUKS_PASSPHRASE="$p1"; break; }
|
||||
warn "Passphrases don't match."
|
||||
done
|
||||
fi
|
||||
WITH_LUKS=false; [[ -n "$LUKS_PASSPHRASE" ]] && WITH_LUKS=true
|
||||
info "Encryption: $([[ $WITH_LUKS == true ]] && echo LUKS2 || echo none)"
|
||||
|
||||
# ─── User account ───────────────────────────────────────────────────────
|
||||
section "Your account"
|
||||
|
||||
if [[ "$UNATTENDED" == "1" ]]; then
|
||||
USERNAME="${NOMARCHY_USERNAME:?}"
|
||||
PASSWORD="${NOMARCHY_PASSWORD:?}"
|
||||
HOSTNAME_="${NOMARCHY_HOSTNAME:-nomarchy}"
|
||||
TIMEZONE="${NOMARCHY_TIMEZONE:-UTC}"
|
||||
else
|
||||
while true; do
|
||||
USERNAME=$(gum input --placeholder "username (lowercase, e.g. ada)")
|
||||
[[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]*$ ]] && break
|
||||
warn "Invalid username (lowercase letters, digits, - and _)."
|
||||
done
|
||||
while true; do
|
||||
PASSWORD=$(gum input --password --placeholder "password for $USERNAME")
|
||||
[[ -n "$PASSWORD" ]] || { warn "Empty password."; continue; }
|
||||
p2=$(gum input --password --placeholder "repeat password")
|
||||
[[ "$PASSWORD" == "$p2" ]] && break
|
||||
warn "Passwords don't match."
|
||||
done
|
||||
while true; do
|
||||
HOSTNAME_=$(gum input --value "nomarchy" --placeholder "hostname")
|
||||
[[ "$HOSTNAME_" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$ ]] && break
|
||||
warn "Invalid hostname."
|
||||
done
|
||||
TIMEZONE=$(timedatectl list-timezones | gum filter --placeholder "timezone (type to search)" || echo UTC)
|
||||
fi
|
||||
[[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]*$ ]] || fail "Invalid username '$USERNAME'."
|
||||
[[ -f "/usr/share/zoneinfo/$TIMEZONE" || -e "/etc/zoneinfo/$TIMEZONE" ]] \
|
||||
|| timedatectl list-timezones 2>/dev/null | grep -qx "$TIMEZONE" \
|
||||
|| warn "Timezone '$TIMEZONE' not verifiable; continuing."
|
||||
HASHED_PASSWORD=$(printf '%s' "$PASSWORD" | mkpasswd -m sha-512 -s)
|
||||
unset PASSWORD
|
||||
info "User: $USERNAME @ $HOSTNAME_ ($TIMEZONE)"
|
||||
|
||||
# ─── Hardware profile ───────────────────────────────────────────────────
|
||||
section "Hardware detection"
|
||||
|
||||
# shellcheck source=hardware-db.sh
|
||||
source "$SHARE/hardware-db.sh"
|
||||
|
||||
HW_PROFILES=()
|
||||
hw_mode="${NOMARCHY_HW:-auto}"
|
||||
if [[ "$hw_mode" == "none" ]]; then
|
||||
info "Hardware profiles skipped."
|
||||
elif [[ "$hw_mode" != "auto" && "$UNATTENDED" == "1" ]]; then
|
||||
read -ra HW_PROFILES <<< "$hw_mode"
|
||||
else
|
||||
detection=$(nomarchy_detect_hw || true)
|
||||
if [[ -n "$detection" ]]; then
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
MODULE\ *) HW_PROFILES+=("${line#MODULE }") ;;
|
||||
DETAIL\ *) info "→ ${line#DETAIL }" ;;
|
||||
esac
|
||||
done <<< "$detection"
|
||||
fi
|
||||
if [[ "$UNATTENDED" != "1" ]]; then
|
||||
if [[ ${#HW_PROFILES[@]} -gt 0 ]] \
|
||||
&& ! gum confirm "Use these nixos-hardware profiles: ${HW_PROFILES[*]}?"; then
|
||||
HW_PROFILES=()
|
||||
picked=$(gum filter --no-limit \
|
||||
--placeholder "pick profiles manually (tab to select, enter to finish, esc for none)" \
|
||||
< "$SHARE/hardware-modules.txt" || true)
|
||||
[[ -n "$picked" ]] && mapfile -t HW_PROFILES <<< "$picked"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
info "Profiles: ${HW_PROFILES[*]:-(none)}"
|
||||
|
||||
# ─── Review & point of no return ────────────────────────────────────────
|
||||
section "Review"
|
||||
|
||||
gum style --border normal --padding "0 2" \
|
||||
"Disk: $TARGET_DISK (WILL BE ERASED)" \
|
||||
"Encryption: $([[ $WITH_LUKS == true ]] && echo LUKS2 || echo none)" \
|
||||
"User: $USERNAME" \
|
||||
"Hostname: $HOSTNAME_" \
|
||||
"Timezone: $TIMEZONE" \
|
||||
"Hardware: ${HW_PROFILES[*]:-none}" \
|
||||
"Offline: $([[ -n "${NOMARCHY_REV:-}" ]] && echo "yes (pinned $NOMARCHY_REV)" || echo "no (will fetch nomarchy from GitHub)")"
|
||||
|
||||
if [[ "$UNATTENDED" != "1" ]]; then
|
||||
typed=$(gum input --placeholder "type the disk name ($(basename "$TARGET_DISK")) to confirm the wipe")
|
||||
[[ "$typed" == "$(basename "$TARGET_DISK")" ]] || fail "Confirmation mismatch — aborting, nothing touched."
|
||||
fi
|
||||
|
||||
# ─── Partitioning ───────────────────────────────────────────────────────
|
||||
section "Partitioning"
|
||||
|
||||
# Pre-wipe: disko gates destructive steps on blkid — on a previously
|
||||
# installed disk it would overlay the old GPT and skip mkfs on a stale
|
||||
# ESP ("wrong fs type, bad superblock" at mount). Wipe first. Teardown
|
||||
# order matters: mounts → swap → LUKS mappings → signatures.
|
||||
prewipe() {
|
||||
local drive="$1" name backing part
|
||||
info "Pre-wiping $drive..."
|
||||
umount -R /mnt 2>/dev/null || true
|
||||
swapoff -a 2>/dev/null || true
|
||||
if command -v dmsetup >/dev/null 2>&1; then
|
||||
while read -r name _; do
|
||||
[[ -n "$name" && "$name" != "No" ]] || continue
|
||||
backing=$(cryptsetup status "$name" 2>/dev/null \
|
||||
| awk '/^[[:space:]]*device:/ { print $2; exit }') || continue
|
||||
[[ "$backing" == "$drive"* ]] || continue
|
||||
info "closing stale LUKS mapping $name"
|
||||
cryptsetup close "$name"
|
||||
done < <(dmsetup ls --target crypt 2>/dev/null)
|
||||
fi
|
||||
if compgen -G "${drive}?*" >/dev/null; then
|
||||
for part in "${drive}"?*; do
|
||||
[[ -b "$part" ]] && wipefs -af "$part" >/dev/null
|
||||
done
|
||||
fi
|
||||
wipefs -af "$drive" >/dev/null
|
||||
sgdisk --zap-all "$drive" >/dev/null
|
||||
# 16 MiB covers LUKS2 headers and the first BTRFS superblock —
|
||||
# wipefs alone misses damaged variants.
|
||||
dd if=/dev/zero of="$drive" bs=1M count=16 conv=fsync status=none
|
||||
partprobe "$drive" 2>/dev/null || true
|
||||
udevadm settle --timeout=30 || info "udevadm settle timed out; continuing."
|
||||
if lsblk -no MOUNTPOINTS "$drive" 2>/dev/null | grep -qE '\S'; then
|
||||
fail "$drive still has active mountpoints after pre-wipe."
|
||||
fi
|
||||
}
|
||||
prewipe "$TARGET_DISK"
|
||||
|
||||
if [[ $WITH_LUKS == true ]]; then
|
||||
install -m 600 /dev/null "$LUKS_KEY_PATH"
|
||||
trap 'rm -f "$LUKS_KEY_PATH" 2>/dev/null || true' EXIT
|
||||
printf '%s' "$LUKS_PASSPHRASE" > "$LUKS_KEY_PATH"
|
||||
unset LUKS_PASSPHRASE
|
||||
fi
|
||||
|
||||
disko_log=$(mktemp --suffix=.disko.log)
|
||||
if ! disko --mode destroy,format,mount --yes-wipe-all-disks \
|
||||
--argstr mainDrive "$TARGET_DISK" \
|
||||
--arg withLuks "$WITH_LUKS" \
|
||||
"$SHARE/disko-config.nix" >"$disko_log" 2>&1; then
|
||||
tail -n 30 "$disko_log"
|
||||
fail "disko failed — full log: $disko_log"
|
||||
fi
|
||||
rm -f "$LUKS_KEY_PATH" "$disko_log"
|
||||
success "Disk partitioned and mounted at /mnt"
|
||||
|
||||
# ─── Configuration generation ───────────────────────────────────────────
|
||||
section "Generating configuration"
|
||||
|
||||
FLAKE_DIR="/mnt/home/$USERNAME/.nomarchy"
|
||||
mkdir -p "$FLAKE_DIR"
|
||||
|
||||
nixos-generate-config --root /mnt
|
||||
mv /mnt/etc/nixos/hardware-configuration.nix "$FLAKE_DIR/"
|
||||
rm -rf /mnt/etc/nixos
|
||||
|
||||
cp "$SHARE/template/home.nix" "$FLAKE_DIR/"
|
||||
cp "$SHARE/template/theme-state.json" "$FLAKE_DIR/"
|
||||
|
||||
hw_nix=""
|
||||
for p in "${HW_PROFILES[@]:-}"; do
|
||||
[[ -n "$p" ]] && hw_nix+=" \"$p\""
|
||||
done
|
||||
|
||||
cat > "$FLAKE_DIR/flake.nix" <<EOF
|
||||
{
|
||||
description = "$HOSTNAME_ — my Nomarchy machine";
|
||||
|
||||
# The only input. nixpkgs, home-manager etc. come pinned through it —
|
||||
# tested together upstream. Generated by nomarchy-install; your machine
|
||||
# lives in system.nix and home.nix, this file is never hand-edited.
|
||||
inputs.nomarchy.url = "${NOMARCHY_FLAKE_URL}";
|
||||
|
||||
outputs = { nomarchy, ... }:
|
||||
nomarchy.lib.mkFlake {
|
||||
src = ./.;
|
||||
username = "$USERNAME";
|
||||
hardwareProfile = [$hw_nix ];
|
||||
};
|
||||
}
|
||||
EOF
|
||||
|
||||
# initialHashedPassword is safe to template: mkpasswd's alphabet is
|
||||
# [a-zA-Z0-9./$] — no Nix string metacharacters.
|
||||
cat > "$FLAKE_DIR/system.nix" <<EOF
|
||||
# Your machine: hostname, users, services. The distro itself comes from
|
||||
# Nomarchy (via flake.nix); override its defaults here with plain NixOS
|
||||
# options, or the nomarchy.system.* toggles.
|
||||
{ pkgs, username, ... }:
|
||||
|
||||
{
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
networking.hostName = "$HOSTNAME_";
|
||||
time.timeZone = "$TIMEZONE";
|
||||
i18n.defaultLocale = "en_US.UTF-8";
|
||||
|
||||
# Your login user — \`username\` flows in from flake.nix automatically.
|
||||
users.users.\${username} = {
|
||||
isNormalUser = true;
|
||||
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
|
||||
initialHashedPassword = "$HASHED_PASSWORD";
|
||||
};
|
||||
|
||||
system.stateVersion = "26.05";
|
||||
}
|
||||
EOF
|
||||
|
||||
# The flake.lock: offline when the ISO knows what it was built from,
|
||||
# otherwise resolve over the network.
|
||||
if [[ -n "${NOMARCHY_REV:-}" ]]; then
|
||||
python3 "$SHARE/compose-lock.py" "$SHARE/flake.lock" "$FLAKE_DIR/flake.lock" \
|
||||
"$NOMARCHY_LOCKED_JSON" "$NOMARCHY_ORIGINAL_JSON"
|
||||
else
|
||||
warn "ISO built from a dirty tree — resolving nomarchy from its forge (needs network)."
|
||||
(cd "$FLAKE_DIR" && nix --extra-experimental-features "nix-command flakes" flake lock)
|
||||
fi
|
||||
|
||||
# A flake worktree must be git-tracked (theme-state.json especially).
|
||||
(
|
||||
cd "$FLAKE_DIR"
|
||||
git init -q
|
||||
git add -A
|
||||
git -c user.name="Nomarchy Installer" -c user.email="installer@nomarchy" \
|
||||
commit -qm "Initial Nomarchy configuration"
|
||||
)
|
||||
|
||||
# /etc/nixos on the installed system points at the user-owned flake.
|
||||
mkdir -p /mnt/etc
|
||||
ln -sfn "/home/$USERNAME/.nomarchy" /mnt/etc/nixos
|
||||
success "Configuration written to ~$USERNAME/.nomarchy"
|
||||
|
||||
# ─── Install ────────────────────────────────────────────────────────────
|
||||
section "Installing (this takes a while)"
|
||||
|
||||
# Seed the target store with the flake source + all inputs so the first
|
||||
# `nomarchy-theme-sync apply` (and the HM pre-activation below) work
|
||||
# before the machine has ever seen a network.
|
||||
info "Seeding flake inputs into the target store..."
|
||||
nix --extra-experimental-features "nix-command flakes" \
|
||||
flake archive --to "local?root=/mnt" "$FLAKE_DIR" >/dev/null \
|
||||
|| warn "flake archive failed — first rebuild will need network."
|
||||
|
||||
nixos-install --no-root-passwd --flake "$FLAKE_DIR#default"
|
||||
success "System installed (bootloader in place)"
|
||||
|
||||
# Pre-activate the Home Manager generation so the FIRST boot lands in the
|
||||
# fully themed desktop, not bare Hyprland. Best-effort: a failure here
|
||||
# only costs the user one `home-manager switch` after logging in.
|
||||
section "Baking the desktop"
|
||||
cat > /mnt/tmp/nomarchy-hm-activate.sh <<EOF
|
||||
set -e
|
||||
export PATH=/run/current-system/sw/bin:\$PATH
|
||||
out=\$(nix --extra-experimental-features "nix-command flakes" \
|
||||
build --offline --no-link --print-out-paths \
|
||||
"/home/$USERNAME/.nomarchy#homeConfigurations.$USERNAME.activationPackage")
|
||||
install -d -o "$USERNAME" -g users /nix/var/nix/profiles/per-user/$USERNAME
|
||||
install -d -o "$USERNAME" -g users /nix/var/nix/gcroots/per-user/$USERNAME
|
||||
runuser -u "$USERNAME" -- bash -lc "USER=$USERNAME HOME=/home/$USERNAME \$out/activate"
|
||||
EOF
|
||||
if nixos-enter --root /mnt -- bash /tmp/nomarchy-hm-activate.sh; then
|
||||
success "Desktop pre-activated — first boot is fully themed"
|
||||
else
|
||||
warn "Desktop pre-activation failed; after first login run:"
|
||||
warn " home-manager switch --flake ~/.nomarchy"
|
||||
fi
|
||||
rm -f /mnt/tmp/nomarchy-hm-activate.sh
|
||||
|
||||
header "Nomarchy installed on $TARGET_DISK" \
|
||||
"User: $USERNAME @ $HOSTNAME_" \
|
||||
"Remove the USB stick when the machine is off."
|
||||
|
||||
finish="${NOMARCHY_FINISH:-ask}"
|
||||
case "$finish" in
|
||||
reboot) systemctl reboot ;;
|
||||
poweroff) systemctl poweroff ;;
|
||||
none) : ;;
|
||||
*) confirm "Reboot into Nomarchy now?" && systemctl reboot || true ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user