Compare commits
23 Commits
f211ef0d09
...
a50e9793ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a50e9793ea | ||
|
|
7b5a22c800 | ||
|
|
134695806c | ||
|
|
727f21005d | ||
|
|
1403ea82b2 | ||
|
|
29818d2fb6 | ||
|
|
77dba7ca5d | ||
|
|
dba349a539 | ||
|
|
3b0b2872aa | ||
|
|
804d708f87 | ||
|
|
8da429d611 | ||
|
|
996fdc28c9 | ||
|
|
609bb7e7dc | ||
|
|
ecf94e84ab | ||
|
|
c813055578 | ||
|
|
8881ad3bae | ||
|
|
7aba5fa0a8 | ||
|
|
479193b10b | ||
|
|
71614fa6ca | ||
|
|
f3707357a6 | ||
|
|
8ded92ea63 | ||
|
|
1f22e996dc | ||
|
|
8a0ec763e0 |
63
README.md
63
README.md
@@ -23,7 +23,7 @@ partial.
|
|||||||
│ Stylix → GTK, Qt, cursors, fonts │
|
│ Stylix → GTK, Qt, cursors, fonts │
|
||||||
└───────────────────────────────────┬──────────────────────────────────────┘
|
└───────────────────────────────────┬──────────────────────────────────────┘
|
||||||
▼
|
▼
|
||||||
wallpaper via swww (the one runtime piece:
|
wallpaper via awww (the one runtime piece:
|
||||||
applied post-switch + at session start, `bg next` cycles)
|
applied post-switch + at session start, `bg next` cycles)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ Flat on purpose. Two module trees, one options file each, no hidden layers.
|
|||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── flake.nix # inputs + the downstream API (exports below)
|
├── flake.nix # inputs + the downstream API (exports below)
|
||||||
|
├── lib.nix # nomarchy.lib.mkFlake — one-call downstream wrapper
|
||||||
├── theme-state.json # ★ THE single source of truth (git-tracked!)
|
├── theme-state.json # ★ THE single source of truth (git-tracked!)
|
||||||
├── themes/ # 21 presets: <slug>.json + optional <slug>/ assets
|
├── themes/ # 21 presets: <slug>.json + optional <slug>/ assets
|
||||||
│ ├── nord.json # palette (required, works alone)
|
│ ├── nord.json # palette (required, works alone)
|
||||||
@@ -57,12 +58,17 @@ Flat on purpose. Two module trees, one options file each, no hidden layers.
|
|||||||
├── hosts/
|
├── hosts/
|
||||||
│ ├── default/ # reference machine (thin: boot, user, hostname)
|
│ ├── default/ # reference machine (thin: boot, user, hostname)
|
||||||
│ └── live.nix # bootable live ISO (try the distro, no install)
|
│ └── 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
|
├── templates/downstream/ # `nix flake init -t` starter for users
|
||||||
├── docs/TESTING.md # how to verify changes (incl. AI-agent rules)
|
├── docs/TESTING.md # how to verify changes (incl. AI-agent rules)
|
||||||
└── tools/ # maintainer-only
|
└── tools/ # maintainer-only
|
||||||
├── import-palettes.py # converts old-distro themes → JSON + assets
|
├── import-palettes.py # converts old-distro themes → JSON + assets
|
||||||
└── test-live-iso.sh # build the ISO + boot it in QEMU
|
├── test-live-iso.sh # build the ISO + boot it in QEMU
|
||||||
|
├── test-install.sh # full offline-install regression in QEMU
|
||||||
|
└── vm/ # headless-VM helpers (QMP keys, VNC shots,
|
||||||
|
# offline-pin gap analysis)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rule of thumb:** `modules/` is the distro (reusable, no machine specifics),
|
**Rule of thumb:** `modules/` is the distro (reusable, no machine specifics),
|
||||||
@@ -85,6 +91,20 @@ the locked inputs into the ISO store — so theme switching (including the
|
|||||||
`home-manager switch` it triggers) works **offline**, exactly like on an
|
`home-manager switch` it triggers) works **offline**, exactly like on an
|
||||||
installed system. Verification checklist: [docs/TESTING.md](docs/TESTING.md).
|
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, LUKS2 full-disk encryption **by default**
|
||||||
|
(in exchange the desktop logs in passwordless — the passphrase already
|
||||||
|
gates the machine), user + hostname + timezone, hardware autodetection
|
||||||
|
(DMI → nixos-hardware profile), a hibernation-ready swapfile sized to RAM,
|
||||||
|
then disko partitions (GPT + ESP + BTRFS subvolumes incl. `@snapshots` —
|
||||||
|
snapper timeline snapshots are on) 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)
|
## 3. Using Nomarchy on your machine (downstream)
|
||||||
|
|
||||||
Nomarchy is consumed as a flake input — you never fork or edit this repo:
|
Nomarchy is consumed as a flake input — you never fork or edit this repo:
|
||||||
@@ -94,8 +114,30 @@ mkdir my-machine && cd my-machine
|
|||||||
nix flake init -t github:YOUR-USER/nomarchy
|
nix flake init -t github:YOUR-USER/nomarchy
|
||||||
```
|
```
|
||||||
|
|
||||||
You own three files: `system.nix`, `home.nix`, and **your own
|
You own two files day-to-day: `system.nix` and `home.nix` (plus
|
||||||
`theme-state.json`**. Two deliberately separate rebuild paths:
|
`theme-state.json`, written by the CLI). Your `flake.nix` is set up once —
|
||||||
|
later by the installer — and never hand-edited; it's a single call:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
outputs = { nomarchy, ... }:
|
||||||
|
nomarchy.lib.mkFlake {
|
||||||
|
src = ./.;
|
||||||
|
username = "me";
|
||||||
|
hardwareProfile = "framework-13-7040-amd"; # optional, nixos-hardware name
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
| `mkFlake` arg | Default | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `src` | — (required) | Your flake directory (`./.`) |
|
||||||
|
| `username` | — (required) | Login name; flows into `system.nix` and names the HM config |
|
||||||
|
| `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` /
|
||||||
|
`homeModules.nomarchy` / `overlays.default` by hand — the wrapper is sugar.)
|
||||||
|
|
||||||
|
Two deliberately separate rebuild paths:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo nixos-rebuild switch --flake .#default # system: rare
|
sudo nixos-rebuild switch --flake .#default # system: rare
|
||||||
@@ -141,9 +183,10 @@ fonts. No runtime patching means no partial states, and `home-manager
|
|||||||
generations` is also your theme history. Waybar even restyles in place: it
|
generations` is also your theme history. Waybar even restyles in place: it
|
||||||
re-reads `style.css` when the symlink flips.
|
re-reads `style.css` when the symlink flips.
|
||||||
|
|
||||||
The **wallpaper** is the one runtime piece (swww is imperative; nothing in
|
The **wallpaper** is the one runtime piece (awww — nixpkgs' swww — is
|
||||||
Nix consumes the path): applied at session start and after every switch via
|
imperative; nothing in Nix consumes the path): applied at session start and
|
||||||
a tiny activation hook, cycled instantly with `bg next`.
|
after every switch via a tiny activation hook, cycled instantly with
|
||||||
|
`bg next`.
|
||||||
|
|
||||||
### Per-theme app assets (`themes/<slug>/`)
|
### Per-theme app assets (`themes/<slug>/`)
|
||||||
|
|
||||||
@@ -192,6 +235,6 @@ workspaces · `Print` region screenshot.
|
|||||||
re-copy on every state write is the main eval tax), then pre-built theme
|
re-copy on every state write is the main eval tax), then pre-built theme
|
||||||
variants if still needed
|
variants if still needed
|
||||||
- Plymouth + SDDM/greeter theming from the same JSON
|
- Plymouth + SDDM/greeter theming from the same JSON
|
||||||
- Interactive installer on the live ISO (disko BTRFS+LUKS, hardware
|
- Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy
|
||||||
profiles) — port from old_distro
|
boot (v1 `nomarchy-install` is single-disk UEFI — see `pkgs/nomarchy-install`)
|
||||||
- `hyprlock`/`hypridle`, swayosd, launch-or-focus UX scripts
|
- `hyprlock`/`hypridle`, swayosd, launch-or-focus UX scripts
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ python3 -m py_compile pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py
|
|||||||
|
|
||||||
`nix flake check` needs a Linux machine (or a Linux builder) since all
|
`nix flake check` needs a Linux machine (or a Linux builder) since all
|
||||||
outputs are `x86_64-linux`. It catches type errors, missing options, and
|
outputs are `x86_64-linux`. It catches type errors, missing options, and
|
||||||
bad merges — most breakage stops here.
|
bad merges — most breakage stops here. It also evaluates the downstream
|
||||||
|
template through `lib.mkFlake` (including a real nixos-hardware profile),
|
||||||
|
so template/wrapper drift fails fast too.
|
||||||
|
|
||||||
## 2. Build and boot the live ISO
|
## 2. Build and boot the live ISO
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ Work through these in order; each one exercises a different layer.
|
|||||||
|
|
||||||
| # | Check | Verifies |
|
| # | Check | Verifies |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 | Boots to Hyprland with Tokyo Night wallpaper visible | greetd autologin, swww, session start |
|
| 1 | Boots to Hyprland with Tokyo Night wallpaper visible | greetd autologin, awww, session start |
|
||||||
| 2 | Waybar shows at top, themed (blue accent on dark) | HM waybar module, palette baking |
|
| 2 | Waybar shows at top, themed (blue accent on dark) | HM waybar module, palette baking |
|
||||||
| 3 | `SUPER+Return` opens Ghostty with Tokyo Night colors | terminal default, ANSI palette |
|
| 3 | `SUPER+Return` opens Ghostty with Tokyo Night colors | terminal default, ANSI palette |
|
||||||
| 4 | `btop` in the terminal is themed | per-theme asset baking |
|
| 4 | `btop` in the terminal is themed | per-theme asset baking |
|
||||||
@@ -65,7 +67,33 @@ 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
|
Items 6 and 8 are the big ones — they prove the all-Home-Manager theming
|
||||||
model end to end.
|
model end to end.
|
||||||
|
|
||||||
## 4. VM-specific gotchas
|
## 4. Testing the installer
|
||||||
|
|
||||||
|
One command runs the whole offline regression:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tools/test-install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
It builds the ISO, boots it in QEMU **with networking disabled**, runs an
|
||||||
|
unattended LUKS+swap install onto a blank disk via the installer's
|
||||||
|
`NOMARCHY_UNATTENDED=1` mode (config delivered on a small vfat disk —
|
||||||
|
typing long commands into a guest drops keystrokes), waits for the
|
||||||
|
installer's poweroff, then boots the installed disk, enters the
|
||||||
|
passphrase, and screenshots the first boot. The machine-checkable parts
|
||||||
|
(install completes, disk boots) fail the script; the visual verdict —
|
||||||
|
**themed desktop, no autogenerated-config banner, no login prompt** — is
|
||||||
|
yours, from `first-boot.png`. The helpers it builds on live in
|
||||||
|
`tools/vm/` (QMP key injection, GL-safe VNC screenshots) and work for any
|
||||||
|
manual VM poking; `tools/vm/gap-analysis.py` is the maintainer tool for
|
||||||
|
diagnosing offline installs that try to build from source.
|
||||||
|
|
||||||
|
To test by hand instead, replicate what `tools/test-install.sh` does: the
|
||||||
|
unattended env it uses is in the script, and the same flow works
|
||||||
|
interactively from the live terminal. If the desktop comes up unthemed,
|
||||||
|
read `/var/log/nomarchy-hm-preactivate.log` on the installed system.
|
||||||
|
|
||||||
|
## 5. VM-specific gotchas
|
||||||
|
|
||||||
- **No KVM** (e.g. nested without acceleration): everything works but
|
- **No KVM** (e.g. nested without acceleration): everything works but
|
||||||
`home-manager switch` will be painfully slow. Don't read slowness as
|
`home-manager switch` will be painfully slow. Don't read slowness as
|
||||||
@@ -77,7 +105,7 @@ model end to end.
|
|||||||
- **First theme switch is the slowest**: it evaluates the flake on a RAM
|
- **First theme switch is the slowest**: it evaluates the flake on a RAM
|
||||||
disk. Subsequent switches reuse the eval cache.
|
disk. Subsequent switches reuse the eval cache.
|
||||||
|
|
||||||
## 5. When something fails
|
## 6. When something fails
|
||||||
|
|
||||||
- Session/login problems: `journalctl -b -u greetd`, then `journalctl
|
- Session/login problems: `journalctl -b -u greetd`, then `journalctl
|
||||||
--user -b`.
|
--user -b`.
|
||||||
|
|||||||
347
flake.lock
generated
Normal file
347
flake.lock
generated
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"base16": {
|
||||||
|
"inputs": {
|
||||||
|
"fromYaml": "fromYaml"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1755819240,
|
||||||
|
"narHash": "sha256-qcMhnL7aGAuFuutH4rq9fvAhCpJWVHLcHVZLtPctPlo=",
|
||||||
|
"owner": "SenchoPens",
|
||||||
|
"repo": "base16.nix",
|
||||||
|
"rev": "75ed5e5e3fce37df22e49125181fa37899c3ccd6",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "SenchoPens",
|
||||||
|
"repo": "base16.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"base16-fish": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1765809053,
|
||||||
|
"narHash": "sha256-XCUQLoLfBJ8saWms2HCIj4NEN+xNsWBlU1NrEPcQG4s=",
|
||||||
|
"owner": "tomyun",
|
||||||
|
"repo": "base16-fish",
|
||||||
|
"rev": "86cbea4dca62e08fb7fd83a70e96472f92574782",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "tomyun",
|
||||||
|
"repo": "base16-fish",
|
||||||
|
"rev": "86cbea4dca62e08fb7fd83a70e96472f92574782",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"base16-helix": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1776754714,
|
||||||
|
"narHash": "sha256-E3OAK27smtATTmX45uoTSRsVD+Y+ZiVVfgM/tjpbtYg=",
|
||||||
|
"owner": "tinted-theming",
|
||||||
|
"repo": "base16-helix",
|
||||||
|
"rev": "4d508123037e7851ad36ebf7d9c48b0e9e1eb581",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "tinted-theming",
|
||||||
|
"repo": "base16-helix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"base16-vim": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1732806396,
|
||||||
|
"narHash": "sha256-e0bpPySdJf0F68Ndanwm+KWHgQiZ0s7liLhvJSWDNsA=",
|
||||||
|
"owner": "tinted-theming",
|
||||||
|
"repo": "base16-vim",
|
||||||
|
"rev": "577fe8125d74ff456cf942c733a85d769afe58b7",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "tinted-theming",
|
||||||
|
"repo": "base16-vim",
|
||||||
|
"rev": "577fe8125d74ff456cf942c733a85d769afe58b7",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"firefox-gnome-theme": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1779670703,
|
||||||
|
"narHash": "sha256-UdfMivNMwCCqQsYDg5pSz8X2IOaOrIZLIIy+Bg3CO2o=",
|
||||||
|
"owner": "rafaelmardojai",
|
||||||
|
"repo": "firefox-gnome-theme",
|
||||||
|
"rev": "942159e73e40bf785816f7f1f5feed9ef3d7c8f9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rafaelmardojai",
|
||||||
|
"repo": "firefox-gnome-theme",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": [
|
||||||
|
"stylix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1778716662,
|
||||||
|
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fromYaml": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731966426,
|
||||||
|
"narHash": "sha256-lq95WydhbUTWig/JpqiB7oViTcHFP8Lv41IGtayokA8=",
|
||||||
|
"owner": "SenchoPens",
|
||||||
|
"repo": "fromYaml",
|
||||||
|
"rev": "106af9e2f715e2d828df706c386a685698f3223b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "SenchoPens",
|
||||||
|
"repo": "fromYaml",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gnome-shell": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1767737596,
|
||||||
|
"narHash": "sha256-eFujfIUQDgWnSJBablOuG+32hCai192yRdrNHTv0a+s=",
|
||||||
|
"owner": "GNOME",
|
||||||
|
"repo": "gnome-shell",
|
||||||
|
"rev": "ef02db02bf0ff342734d525b5767814770d85b49",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "GNOME",
|
||||||
|
"repo": "gnome-shell",
|
||||||
|
"rev": "ef02db02bf0ff342734d525b5767814770d85b49",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"home-manager": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1781063120,
|
||||||
|
"narHash": "sha256-1UIF/mDJluwJQjmmcZ2j1L2+mjYsefe82QCLj0TYSOg=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "home-manager",
|
||||||
|
"rev": "baa46aeb6d02e0ba13de67cd35e3d57aedfacf01",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"ref": "release-26.05",
|
||||||
|
"repo": "home-manager",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixos-hardware": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1781020964,
|
||||||
|
"narHash": "sha256-fS7xTi2j2iso5Hj7RNZLv/acDlCT+fgMVkVk40A7Uco=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixos-hardware",
|
||||||
|
"rev": "32c2cd9e46286c4eced3dc6b613c659126bf3cca",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "master",
|
||||||
|
"repo": "nixos-hardware",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1780902259,
|
||||||
|
"narHash": "sha256-q8yYEC5f1mFlQO9RGna4LTc9QrcvWunX6FYp83munkQ=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "bd0ff2d3eac24699c3664d5966b9ef36f388e2ca",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-26.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nur": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-parts": [
|
||||||
|
"stylix",
|
||||||
|
"flake-parts"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"stylix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1780281641,
|
||||||
|
"narHash": "sha256-M/+hUKoKbHXpV0xGVfELbN1Ds1aoe3pL5p5/t46YhVo=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "NUR",
|
||||||
|
"rev": "30f9ae2f04174de63ba8bcf3580ca90843b28a01",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "NUR",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"home-manager": "home-manager",
|
||||||
|
"nixos-hardware": "nixos-hardware",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"stylix": "stylix"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stylix": {
|
||||||
|
"inputs": {
|
||||||
|
"base16": "base16",
|
||||||
|
"base16-fish": "base16-fish",
|
||||||
|
"base16-helix": "base16-helix",
|
||||||
|
"base16-vim": "base16-vim",
|
||||||
|
"firefox-gnome-theme": "firefox-gnome-theme",
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
|
"gnome-shell": "gnome-shell",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"nur": "nur",
|
||||||
|
"systems": "systems",
|
||||||
|
"tinted-kitty": "tinted-kitty",
|
||||||
|
"tinted-schemes": "tinted-schemes",
|
||||||
|
"tinted-tmux": "tinted-tmux",
|
||||||
|
"tinted-zed": "tinted-zed"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1781018772,
|
||||||
|
"narHash": "sha256-C+cGIUaC6dqfwTbI+BwCd572PbESGA3WYxR1sLTqxkY=",
|
||||||
|
"owner": "danth",
|
||||||
|
"repo": "stylix",
|
||||||
|
"rev": "a378e4c09031fb15a4d65da88aa628f71fc52f6b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "danth",
|
||||||
|
"repo": "stylix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tinted-kitty": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1735730497,
|
||||||
|
"narHash": "sha256-4KtB+FiUzIeK/4aHCKce3V9HwRvYaxX+F1edUrfgzb8=",
|
||||||
|
"owner": "tinted-theming",
|
||||||
|
"repo": "tinted-kitty",
|
||||||
|
"rev": "de6f888497f2c6b2279361bfc790f164bfd0f3fa",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "tinted-theming",
|
||||||
|
"repo": "tinted-kitty",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tinted-schemes": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1777806186,
|
||||||
|
"narHash": "sha256-PDF0/wObw4nIsSBeXVYLsloXOiphXCgIdsrNcVXguKs=",
|
||||||
|
"owner": "tinted-theming",
|
||||||
|
"repo": "schemes",
|
||||||
|
"rev": "0c94645546f4f3ddac77a1a5fce54eb95bf50795",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "tinted-theming",
|
||||||
|
"repo": "schemes",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tinted-tmux": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1778379944,
|
||||||
|
"narHash": "sha256-wPDFzMGSlARlw0Sfsn48Q2+jPSfk6N0Ng6BC/d+7Q24=",
|
||||||
|
"owner": "tinted-theming",
|
||||||
|
"repo": "tinted-tmux",
|
||||||
|
"rev": "fe0203a198690e71a5ff11e08812a4673de3678d",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "tinted-theming",
|
||||||
|
"repo": "tinted-tmux",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tinted-zed": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1778378178,
|
||||||
|
"narHash": "sha256-OXPXRIQgGwV77HjYRryOHguh4ALX96jkg+tseLkGgHA=",
|
||||||
|
"owner": "tinted-theming",
|
||||||
|
"repo": "base16-zed",
|
||||||
|
"rev": "9cd816033ff969415b190722cddf134e78a5665f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "tinted-theming",
|
||||||
|
"repo": "base16-zed",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
194
flake.nix
194
flake.nix
@@ -14,9 +14,17 @@
|
|||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
inputs.home-manager.follows = "home-manager";
|
inputs.home-manager.follows = "home-manager";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Pinned by Nomarchy so distro + hardware quirks are tested together
|
||||||
|
# (consumed by lib.mkFlake's hardwareProfile). Its nixpkgs input is
|
||||||
|
# only used by its own CI — follow ours to keep locks/ISO lean.
|
||||||
|
nixos-hardware = {
|
||||||
|
url = "github:NixOS/nixos-hardware/master";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, home-manager, stylix, ... }:
|
outputs = { self, nixpkgs, home-manager, stylix, nixos-hardware, ... }:
|
||||||
let
|
let
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
username = "nomarchy"; # reference host only; downstream picks its own
|
username = "nomarchy"; # reference host only; downstream picks its own
|
||||||
@@ -25,6 +33,31 @@
|
|||||||
inherit system;
|
inherit system;
|
||||||
overlays = [ self.overlays.default ];
|
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));
|
||||||
|
nixpkgsPath = nixpkgs.outPath;
|
||||||
|
flakeUrl = "git+${gitUrl}";
|
||||||
|
# Future updates re-resolve from the forge (original); the install
|
||||||
|
# itself resolves from the ISO store (locked = path) — fully
|
||||||
|
# offline, even from a dirty-tree ISO.
|
||||||
|
originalNode = { type = "git"; url = gitUrl; };
|
||||||
|
lockedNode = {
|
||||||
|
type = "path";
|
||||||
|
path = self.outPath;
|
||||||
|
narHash = self.narHash;
|
||||||
|
lastModified = self.lastModified or 0;
|
||||||
|
};
|
||||||
|
rev = self.rev or null;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# ─── Downstream API ────────────────────────────────────────────
|
# ─── Downstream API ────────────────────────────────────────────
|
||||||
@@ -52,6 +85,14 @@
|
|||||||
imports = [ stylix.homeModules.stylix ./modules/home ];
|
imports = [ stylix.homeModules.stylix ./modules/home ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# One-call downstream wrapper — see templates/downstream/flake.nix.
|
||||||
|
# The generated flake is never hand-edited; machines live in
|
||||||
|
# system.nix / home.nix.
|
||||||
|
lib = import ./lib.nix {
|
||||||
|
inherit nixpkgs home-manager nixos-hardware;
|
||||||
|
nomarchy = self;
|
||||||
|
};
|
||||||
|
|
||||||
templates.default = {
|
templates.default = {
|
||||||
path = ./templates/downstream;
|
path = ./templates/downstream;
|
||||||
description = "Nomarchy machine configuration (downstream flake)";
|
description = "Nomarchy machine configuration (downstream flake)";
|
||||||
@@ -59,10 +100,35 @@
|
|||||||
|
|
||||||
packages.${system} = {
|
packages.${system} = {
|
||||||
nomarchy-theme-sync = pkgs.nomarchy-theme-sync;
|
nomarchy-theme-sync = pkgs.nomarchy-theme-sync;
|
||||||
|
nomarchy-install = nomarchyInstall;
|
||||||
default = pkgs.nomarchy-theme-sync;
|
default = pkgs.nomarchy-theme-sync;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# ─── Checks ────────────────────────────────────────────────────
|
||||||
|
# `nix flake check` never evaluates templates/, so exercise the
|
||||||
|
# downstream template through mkFlake directly (this bypasses the
|
||||||
|
# template's own three-line flake.nix, which only resolves once
|
||||||
|
# instantiated — everything else is covered here, including the
|
||||||
|
# hardwareProfile → nixos-hardware mapping).
|
||||||
|
checks.${system} =
|
||||||
|
let
|
||||||
|
downstream = self.lib.mkFlake {
|
||||||
|
src = ./templates/downstream;
|
||||||
|
username = "me";
|
||||||
|
hardwareProfile = "framework-13-7040-amd"; # exercises the mapping
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
downstream-template-system =
|
||||||
|
downstream.nixosConfigurations.default.config.system.build.toplevel;
|
||||||
|
downstream-template-home =
|
||||||
|
downstream.homeConfigurations.me.activationPackage;
|
||||||
|
};
|
||||||
|
|
||||||
# ─── Reference host ────────────────────────────────────────────
|
# ─── Reference host ────────────────────────────────────────────
|
||||||
|
# Manually wired rather than via lib.mkFlake: the repo-root
|
||||||
|
# theme-state.json is load-bearing (live ISO + in-repo CLI dev),
|
||||||
|
# which doesn't match mkFlake's src-layout convention.
|
||||||
# Deliberately split rebuild paths:
|
# Deliberately split rebuild paths:
|
||||||
# system → sudo nixos-rebuild switch --flake .#nomarchy (rare)
|
# system → sudo nixos-rebuild switch --flake .#nomarchy (rare)
|
||||||
# desktop → home-manager switch --flake .#nomarchy (no sudo;
|
# desktop → home-manager switch --flake .#nomarchy (no sudo;
|
||||||
@@ -122,15 +188,131 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# The standalone CLI that `nomarchy-theme-sync apply` runs for
|
||||||
|
# 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
|
# Keep the locked flake inputs in the ISO store so theme
|
||||||
# switching on the live system works without a network: the
|
# switching on the live system works without a network: the
|
||||||
# lockfile's narHashes resolve against these paths instead
|
# lockfile's narHashes resolve against these paths instead
|
||||||
# of fetching from GitHub.
|
# of fetching from GitHub. Must be *transitive* — evaluating
|
||||||
system.extraDependencies = [
|
# the seeded flake also fetches stylix's own inputs.
|
||||||
nixpkgs.outPath
|
system.extraDependencies =
|
||||||
home-manager.outPath
|
let
|
||||||
stylix.outPath
|
collectInputs = input:
|
||||||
|
[ input.outPath ] ++ builtins.concatMap collectInputs
|
||||||
|
(builtins.attrValues (input.inputs or { }));
|
||||||
|
in
|
||||||
|
collectInputs self ++ (
|
||||||
|
# Pre-build a system shaped like what nomarchy-install
|
||||||
|
# GENERATES (snapper, auto-login, LUKS initrd, swapfile +
|
||||||
|
# resume — not the bare template!) plus the template
|
||||||
|
# desktop, so the offline install never builds packages
|
||||||
|
# from source: a custom username/hostname then changes
|
||||||
|
# only a handful of derivations. No hardware profile —
|
||||||
|
# matches a VM install; profiled installs share most of it.
|
||||||
|
let
|
||||||
|
template = self.lib.mkFlake {
|
||||||
|
src = ./templates/downstream;
|
||||||
|
username = username;
|
||||||
|
};
|
||||||
|
representativeInstall = nixpkgs.lib.nixosSystem {
|
||||||
|
inherit system;
|
||||||
|
specialArgs = { inherit username; };
|
||||||
|
modules = [
|
||||||
|
self.nixosModules.nomarchy
|
||||||
|
{ environment.systemPackages = [ home-manager.packages.${system}.home-manager ]; }
|
||||||
|
# The installer's autodetection emits these on real
|
||||||
|
# machines (and the intel CPU module pulls the whole
|
||||||
|
# iGPU stack) — without them an offline install on
|
||||||
|
# common hardware builds graphics drivers from
|
||||||
|
# source. Model-specific profiles may still need
|
||||||
|
# network; the common ground is covered.
|
||||||
|
nixos-hardware.nixosModules.common-cpu-intel
|
||||||
|
nixos-hardware.nixosModules.common-cpu-amd
|
||||||
|
nixos-hardware.nixosModules.common-gpu-amd
|
||||||
|
nixos-hardware.nixosModules.common-pc-laptop
|
||||||
|
nixos-hardware.nixosModules.common-pc-ssd
|
||||||
|
nixos-hardware.nixosModules.common-pc
|
||||||
|
(./templates/downstream + "/hardware-configuration.nix")
|
||||||
|
(./templates/downstream + "/system.nix")
|
||||||
|
({ lib, ... }: {
|
||||||
|
nomarchy.system.snapper.enable = true;
|
||||||
|
nomarchy.system.greeter.autoLogin = username;
|
||||||
|
swapDevices = [{ device = "/swap/swapfile"; }];
|
||||||
|
boot.resumeDevice = "/dev/disk/by-uuid/00000000-0000-0000-0000-000000000000";
|
||||||
|
boot.kernelParams = [ "resume_offset=1" ];
|
||||||
|
boot.initrd.luks.devices.crypted.device = "/dev/disk/by-partlabel/disk-main-root";
|
||||||
|
# The real install is BTRFS with a vfat ESP — the
|
||||||
|
# placeholder's bare ext4 would leave fs-gated bits
|
||||||
|
# out of this pin (snapper via btrfs; mtools and
|
||||||
|
# dosfstools enter systemPackages via vfat!).
|
||||||
|
fileSystems."/" = lib.mkForce {
|
||||||
|
device = "/dev/mapper/crypted";
|
||||||
|
fsType = "btrfs";
|
||||||
|
options = [ "subvol=@" "compress=zstd" "noatime" ];
|
||||||
|
};
|
||||||
|
fileSystems."/boot" = {
|
||||||
|
device = "/dev/disk/by-uuid/0000-0000";
|
||||||
|
fsType = "vfat";
|
||||||
|
options = [ "umask=0077" ];
|
||||||
|
};
|
||||||
|
})
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
in [
|
||||||
|
representativeInstall.config.system.build.toplevel
|
||||||
|
template.homeConfigurations.${username}.activationPackage
|
||||||
|
|
||||||
|
# A real install rebuilds the drvs that embed the
|
||||||
|
# user's hostname/username/UUIDs (etc, initrd, units,
|
||||||
|
# toplevel, HM activation). inputDerivation pins each
|
||||||
|
# one's direct build inputs…
|
||||||
|
representativeInstall.config.system.build.toplevel.inputDerivation
|
||||||
|
representativeInstall.config.system.build.etc.inputDerivation
|
||||||
|
representativeInstall.config.system.build.initialRamdisk.inputDerivation
|
||||||
|
template.homeConfigurations.${username}.activationPackage.inputDerivation
|
||||||
|
] ++ [
|
||||||
|
# …and the second-level build tools those rebuilds need
|
||||||
|
# (their parents are themselves rebuilt, so parent
|
||||||
|
# inputDerivations don't reach them): kernel-module
|
||||||
|
# shrinking, unit-dir assembly, dbus config, /etc.
|
||||||
|
pkgs.kmod.dev
|
||||||
|
pkgs.nukeReferences
|
||||||
|
pkgs.xorg.lndir
|
||||||
|
pkgs.libxslt.bin
|
||||||
|
pkgs.python3Minimal
|
||||||
|
# system-path is rebuilt on EVERY machine: buildEnv's
|
||||||
|
# package-list order follows module-graph order, and any
|
||||||
|
# hardware profile/import reshuffles it. Its builder
|
||||||
|
# wants these.
|
||||||
|
pkgs.texinfo
|
||||||
|
pkgs.getconf
|
||||||
|
# greetd.toml embeds the auto-login user and is generated
|
||||||
|
# by remarshal — without this pin an offline install
|
||||||
|
# tries to BUILD remarshal, whose test closure pulls
|
||||||
|
# matplotlib/ffmpeg/x265 (the ~1900-drv cascade).
|
||||||
|
pkgs.remarshal
|
||||||
|
|
||||||
|
# …and one representative disko script: its tool closure
|
||||||
|
# (file, which, wrapper hooks, …) isn't otherwise on the
|
||||||
|
# ISO, and offline nix would try to build it from source.
|
||||||
|
# The user's actual args produce a different script drv,
|
||||||
|
# but with identical inputs — built offline in seconds.
|
||||||
|
(import "${pkgs.disko}/share/disko/cli.nix" {
|
||||||
|
inherit pkgs;
|
||||||
|
mode = "destroy,format,mount";
|
||||||
|
diskoFile = ./pkgs/nomarchy-install/disko-config.nix;
|
||||||
|
mainDrive = "/dev/vda";
|
||||||
|
withLuks = true;
|
||||||
|
swapSize = "2G";
|
||||||
|
})
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
The graphical session autologins as '${username}' (no password).
|
The graphical session autologins as '${username}' (no password).
|
||||||
Theme switching: nomarchy-theme-sync apply <name> (or SUPER+T)
|
Theme switching: nomarchy-theme-sync apply <name> (or SUPER+T)
|
||||||
Wallpapers: nomarchy-theme-sync bg next (or SUPER+SHIFT+T)
|
Wallpapers: nomarchy-theme-sync bg next (or SUPER+SHIFT+T)
|
||||||
|
Install to disk: nomarchy-install
|
||||||
The flake lives at ~/.nomarchy.
|
The flake lives at ~/.nomarchy.
|
||||||
'';
|
'';
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@
|
|||||||
# Welcome toast once the session is up (concatenated onto the
|
# Welcome toast once the session is up (concatenated onto the
|
||||||
# base exec-once list).
|
# base exec-once list).
|
||||||
exec-once = [
|
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\"'"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
93
lib.nix
Normal file
93
lib.nix
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Downstream sugar: one call wires a whole machine flake.
|
||||||
|
# Users own ONLY system.nix, home.nix and theme-state.json; their flake.nix
|
||||||
|
# is generated once (by `nix flake init -t` or the future installer) and
|
||||||
|
# never hand-edited. The raw exports (nixosModules/homeModules/overlays)
|
||||||
|
# in flake.nix remain the escape hatch for power users.
|
||||||
|
{ nixpkgs, home-manager, nixos-hardware, nomarchy }:
|
||||||
|
|
||||||
|
{
|
||||||
|
mkFlake =
|
||||||
|
{ src # the downstream flake directory (./.)
|
||||||
|
, username # login name; also the homeConfigurations attr
|
||||||
|
, hardwareProfile ? null # nixos-hardware module name(s): string, list, or null
|
||||||
|
, system ? "x86_64-linux"
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
inherit (nixpkgs) lib;
|
||||||
|
|
||||||
|
# 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 "${name}".
|
||||||
|
${let
|
||||||
|
close = builtins.filter
|
||||||
|
(candidate: lib.strings.levenshteinAtMost 3 candidate name)
|
||||||
|
(builtins.attrNames nixos-hardware.nixosModules);
|
||||||
|
in
|
||||||
|
lib.optionalString (close != [ ])
|
||||||
|
"Did you mean: ${lib.concatStringsSep ", " close}?\n"
|
||||||
|
}Valid names are the attribute names of nixos-hardware.nixosModules:
|
||||||
|
https://github.com/NixOS/nixos-hardware
|
||||||
|
or list them locally with:
|
||||||
|
nix eval github:NixOS/nixos-hardware#nixosModules --apply builtins.attrNames
|
||||||
|
Omit hardwareProfile entirely if your machine has no profile.
|
||||||
|
'';
|
||||||
|
|
||||||
|
hardwareModules = map lookupProfile profileNames;
|
||||||
|
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ nomarchy.overlays.default ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# System layer — rebuilt rarely:
|
||||||
|
# sudo nixos-rebuild switch --flake .#default
|
||||||
|
nixosConfigurations.default = nixpkgs.lib.nixosSystem {
|
||||||
|
inherit system;
|
||||||
|
specialArgs = { inherit username; };
|
||||||
|
modules =
|
||||||
|
[
|
||||||
|
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
|
||||||
|
(src + "/system.nix")
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Desktop layer — every theme change, no sudo:
|
||||||
|
# home-manager switch --flake .#<username>
|
||||||
|
# (`nomarchy-theme-sync apply` runs this for you.)
|
||||||
|
homeConfigurations.${username} = home-manager.lib.homeManagerConfiguration {
|
||||||
|
inherit pkgs;
|
||||||
|
modules = [
|
||||||
|
nomarchy.homeModules.nomarchy
|
||||||
|
(src + "/home.nix")
|
||||||
|
{
|
||||||
|
# Written by nomarchy-theme-sync; reading it is pure — the
|
||||||
|
# file is part of the downstream flake's source.
|
||||||
|
nomarchy.stateFile = src + "/theme-state.json";
|
||||||
|
home = {
|
||||||
|
inherit username;
|
||||||
|
homeDirectory = "/home/${username}";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
home.stateVersion = "26.05";
|
home.stateVersion = "26.05";
|
||||||
|
|
||||||
home.packages = with pkgs; [
|
home.packages = with pkgs; [
|
||||||
swww # wallpaper daemon with animated transitions
|
awww # wallpaper daemon with animated transitions (the swww fork)
|
||||||
fuzzel # launcher / dmenu (used by the theme picker bind)
|
fuzzel # launcher / dmenu (used by the theme picker bind)
|
||||||
libnotify
|
libnotify
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ in
|
|||||||
package = null;
|
package = null;
|
||||||
portalPackage = null;
|
portalPackage = null;
|
||||||
|
|
||||||
|
# HM defaults stateVersion >= 26.05 to the new Lua config and would
|
||||||
|
# render these hyprlang-style settings as broken `hl.$mod(...)` Lua
|
||||||
|
# (Hyprland then boots into emergency mode with no binds).
|
||||||
|
configType = "hyprlang";
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
"$mod" = "SUPER";
|
"$mod" = "SUPER";
|
||||||
"$terminal" = config.nomarchy.terminal;
|
"$terminal" = config.nomarchy.terminal;
|
||||||
@@ -33,9 +38,10 @@ in
|
|||||||
monitor = [ ",preferred,auto,1" ];
|
monitor = [ ",preferred,auto,1" ];
|
||||||
|
|
||||||
exec-once = [
|
exec-once = [
|
||||||
"swww-daemon"
|
# nixpkgs' swww is awww (renamed fork); the binary is awww-daemon.
|
||||||
|
"awww-daemon"
|
||||||
# Paint the wallpaper as soon as the session is up (waits for
|
# Paint the wallpaper as soon as the session is up (waits for
|
||||||
# swww-daemon internally).
|
# the daemon internally).
|
||||||
"nomarchy-theme-sync wallpaper"
|
"nomarchy-theme-sync wallpaper"
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -89,13 +95,13 @@ in
|
|||||||
touchpad.natural_scroll = true;
|
touchpad.natural_scroll = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
dwindle = {
|
# dwindle:pseudotile was removed in Hyprland 0.55 (the `pseudo`
|
||||||
pseudotile = true;
|
# dispatcher still exists); setting it aborts config parsing.
|
||||||
preserve_split = true;
|
dwindle.preserve_split = true;
|
||||||
};
|
|
||||||
|
|
||||||
misc = {
|
misc = {
|
||||||
disable_hyprland_logo = true;
|
disable_hyprland_logo = true;
|
||||||
|
disable_splash_rendering = true;
|
||||||
force_default_wallpaper = 0;
|
force_default_wallpaper = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,17 @@ in
|
|||||||
|
|
||||||
services.greetd = lib.mkIf cfg.greeter.enable {
|
services.greetd = lib.mkIf cfg.greeter.enable {
|
||||||
enable = lib.mkDefault true;
|
enable = lib.mkDefault true;
|
||||||
settings.default_session = {
|
settings = {
|
||||||
command = lib.mkDefault "${pkgs.greetd.tuigreet}/bin/tuigreet --time --remember --cmd Hyprland";
|
default_session = {
|
||||||
|
command = lib.mkDefault "${pkgs.tuigreet}/bin/tuigreet --time --remember --cmd Hyprland";
|
||||||
user = "greeter";
|
user = "greeter";
|
||||||
};
|
};
|
||||||
|
# Boot straight into the session once; logout → normal greeter.
|
||||||
|
initial_session = lib.mkIf (cfg.greeter.autoLogin != null) {
|
||||||
|
command = "Hyprland";
|
||||||
|
user = cfg.greeter.autoLogin;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# ── Audio: Pipewire ──────────────────────────────────────────────
|
# ── Audio: Pipewire ──────────────────────────────────────────────
|
||||||
@@ -53,6 +60,24 @@ in
|
|||||||
hardware.bluetooth.enable = lib.mkDefault cfg.bluetooth.enable;
|
hardware.bluetooth.enable = lib.mkDefault cfg.bluetooth.enable;
|
||||||
services.blueman.enable = lib.mkDefault cfg.bluetooth.enable;
|
services.blueman.enable = lib.mkDefault cfg.bluetooth.enable;
|
||||||
|
|
||||||
|
# ── BTRFS timeline snapshots (ported from the previous iteration) ─
|
||||||
|
# Guarded on the actual filesystem so enabling it on an ext4 machine
|
||||||
|
# is a clean no-op rather than a failing timer.
|
||||||
|
services.snapper.configs = lib.mkIf
|
||||||
|
(cfg.snapper.enable && (config.fileSystems."/".fsType or "") == "btrfs")
|
||||||
|
{
|
||||||
|
root = {
|
||||||
|
SUBVOLUME = "/";
|
||||||
|
TIMELINE_CREATE = true;
|
||||||
|
TIMELINE_CLEANUP = true;
|
||||||
|
TIMELINE_LIMIT_HOURLY = "5";
|
||||||
|
TIMELINE_LIMIT_DAILY = "7";
|
||||||
|
TIMELINE_LIMIT_WEEKLY = "0";
|
||||||
|
TIMELINE_LIMIT_MONTHLY = "0";
|
||||||
|
TIMELINE_LIMIT_YEARLY = "0";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# ── Fonts ────────────────────────────────────────────────────────
|
# ── Fonts ────────────────────────────────────────────────────────
|
||||||
fonts = {
|
fonts = {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
@@ -82,7 +107,21 @@ in
|
|||||||
wl-clipboard
|
wl-clipboard
|
||||||
grim
|
grim
|
||||||
slurp
|
slurp
|
||||||
];
|
] ++ lib.optional (cfg.snapper.enable && (config.fileSystems."/".fsType or "") == "btrfs")
|
||||||
|
# Snapshot, then rebuild — rollback material for system changes
|
||||||
|
# (theme changes don't need it; HM generations already roll back).
|
||||||
|
(pkgs.writeShellScriptBin "nixos-rebuild-snap" ''
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "This script must be run as root (use sudo)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Creating pre-rebuild snapshot..."
|
||||||
|
${pkgs.snapper}/bin/snapper -c root create \
|
||||||
|
-d "Pre-rebuild $(date +'%Y-%m-%d %H:%M:%S')" \
|
||||||
|
--cleanup-algorithm number
|
||||||
|
echo "Rebuilding..."
|
||||||
|
nixos-rebuild switch --flake /etc/nixos#default "$@"
|
||||||
|
'');
|
||||||
|
|
||||||
# ── Nix itself ───────────────────────────────────────────────────
|
# ── Nix itself ───────────────────────────────────────────────────
|
||||||
nix = {
|
nix = {
|
||||||
|
|||||||
@@ -8,7 +8,26 @@
|
|||||||
{
|
{
|
||||||
options.nomarchy.system = {
|
options.nomarchy.system = {
|
||||||
greeter.enable = lib.mkEnableOption "the greetd/tuigreet login screen" // { default = true; };
|
greeter.enable = lib.mkEnableOption "the greetd/tuigreet login screen" // { default = true; };
|
||||||
|
|
||||||
|
greeter.autoLogin = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "ada";
|
||||||
|
description = ''
|
||||||
|
Log this user straight into Hyprland on boot (greetd
|
||||||
|
initial_session); logging out lands on the normal greeter.
|
||||||
|
The installer sets it on LUKS-encrypted machines — the disk
|
||||||
|
passphrase already gates access, a second prompt is ceremony.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; };
|
audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; };
|
||||||
bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // { default = true; };
|
bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // { default = true; };
|
||||||
|
|
||||||
|
snapper.enable = lib.mkEnableOption ''
|
||||||
|
hourly/daily BTRFS timeline snapshots of / via snapper, plus the
|
||||||
|
`nixos-rebuild-snap` pre-rebuild-snapshot helper. No-op unless the
|
||||||
|
root filesystem is BTRFS with a /.snapshots subvolume (the installer
|
||||||
|
creates one)'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
67
pkgs/nomarchy-install/compose-lock.py
Normal file
67
pkgs/nomarchy-install/compose-lock.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/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("narHash"):
|
||||||
|
sys.exit("compose-lock: no narHash in locked metadata")
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Follows paths (list-valued inputs) are absolute from the lock's
|
||||||
|
# root; the copied nodes now live one level down, so ["nixpkgs"]
|
||||||
|
# must become ["nomarchy", "nixpkgs"]. Plain string values reference
|
||||||
|
# node keys directly and stay as they are.
|
||||||
|
for node in nodes.values():
|
||||||
|
for name, ref in node.get("inputs", {}).items():
|
||||||
|
if isinstance(ref, list):
|
||||||
|
node["inputs"][name] = ["nomarchy"] + ref
|
||||||
|
|
||||||
|
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")
|
||||||
|
pin = locked.get("rev", locked["narHash"])[:12]
|
||||||
|
print(f"compose-lock: wrote {out} (nomarchy @ {pin})")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
78
pkgs/nomarchy-install/default.nix
Normal file
78
pkgs/nomarchy-install/default.nix
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{ 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
|
||||||
|
, btrfs-progs # inspect-internal map-swapfile (hibernation offset)
|
||||||
|
# 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
|
||||||
|
, nixpkgsPath # pinned nixpkgs source (NIX_PATH for disko's eval)
|
||||||
|
, flakeUrl # flake-style URL written into the generated flake.nix
|
||||||
|
, lockedNode # attrset: the flake.lock "locked" node for nomarchy
|
||||||
|
# (path-type: resolves from the ISO store, offline)
|
||||||
|
, originalNode # attrset: the flake.lock "original" node (the forge,
|
||||||
|
# so `nix flake update` later re-resolves normally)
|
||||||
|
, rev ? null # informational: rev the ISO was built from
|
||||||
|
}:
|
||||||
|
|
||||||
|
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"
|
||||||
|
# Empty flake registry: no network lookups for indirect refs.
|
||||||
|
echo '{"flakes":[],"version":2}' > "$share/registry.json"
|
||||||
|
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 btrfs-progs
|
||||||
|
]} \
|
||||||
|
--set NOMARCHY_INSTALL_SHARE "$share" \
|
||||||
|
--set NOMARCHY_NIXPKGS ${nixpkgsPath} \
|
||||||
|
--set NOMARCHY_FLAKE_URL ${lib.escapeShellArg flakeUrl} \
|
||||||
|
--set NOMARCHY_REV ${lib.escapeShellArg (toString rev)} \
|
||||||
|
--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;
|
||||||
|
};
|
||||||
|
}
|
||||||
81
pkgs/nomarchy-install/disko-config.nix
Normal file
81
pkgs/nomarchy-install/disko-config.nix
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# 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 \
|
||||||
|
# --argstr swapSize 16G \
|
||||||
|
# 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
|
||||||
|
, swapSize ? "0" # "0" = no swap; otherwise e.g. "16G" (sized for hibernation)
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
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; };
|
||||||
|
# snapper timeline snapshots (nomarchy.system.snapper.enable)
|
||||||
|
"@snapshots" = { mountpoint = "/.snapshots"; mountOptions = btrfsMountOptions; };
|
||||||
|
} // (if swapSize == "0" then { } else {
|
||||||
|
# Hibernation-ready swapfile on its own subvolume; disko's
|
||||||
|
# mkswapfile handles the BTRFS NOCOW requirements.
|
||||||
|
"@swap" = {
|
||||||
|
mountpoint = "/swap";
|
||||||
|
swap.swapfile.size = swapSize;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
179
pkgs/nomarchy-install/hardware-db.sh
Normal file
179
pkgs/nomarchy-install/hardware-db.sh
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# 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 (glob test, not compgen — nixpkgs' non-interactive bash
|
||||||
|
# is built without the completion builtins)
|
||||||
|
local bats=(/sys/class/power_supply/BAT*)
|
||||||
|
if [[ -e "${bats[0]}" ]]; 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 ))
|
||||||
|
}
|
||||||
511
pkgs/nomarchy-install/nomarchy-install.sh
Normal file
511
pkgs/nomarchy-install/nomarchy-install.sh
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
#!/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_SWAP_GB=N (default: RAM size; 0 = none)] \
|
||||||
|
# [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."
|
||||||
|
|
||||||
|
# Offline-proof nix: disko's eval resolves <nixpkgs> (a dead channel on
|
||||||
|
# the live ISO) and flake commands may consult the global registry —
|
||||||
|
# point both at what the ISO already carries.
|
||||||
|
export NIX_PATH="nixpkgs=$NOMARCHY_NIXPKGS"
|
||||||
|
export NIX_CONFIG="flake-registry = $SHARE/registry.json"
|
||||||
|
|
||||||
|
# With no network, every substituter query is a DNS timeout + retry storm
|
||||||
|
# (and tickles a nix goal.cc assertion crash). Everything an install needs
|
||||||
|
# is pinned into the ISO — drop the binary caches and substitute from the
|
||||||
|
# live store's daemon instead. The explicit daemon substituter matters:
|
||||||
|
# nixos-install builds with --store /mnt and its own "auto?trusted=1"
|
||||||
|
# substituter resolves to the TARGET store (i.e. itself), so without
|
||||||
|
# this nothing flows from the ISO and nix bootstraps gcc from source.
|
||||||
|
NIXOS_INSTALL_OPTS=()
|
||||||
|
if ! timeout 3 bash -c '</dev/tcp/cache.nixos.org/443' 2>/dev/null; then
|
||||||
|
info "No network — substituting from the ISO store only."
|
||||||
|
NIX_CONFIG+=$'\nsubstituters =\nextra-substituters = daemon?trusted=1\nbuilders ='
|
||||||
|
# Must ALSO go through nixos-install as a flag: it passes its own
|
||||||
|
# --extra-substituters "auto?trusted=1", and flags override the env
|
||||||
|
# config for the same setting — without this the in-target build
|
||||||
|
# substitutes from itself (= nothing) and bootstraps gcc from source.
|
||||||
|
NIXOS_INSTALL_OPTS+=(--substituters "daemon?trusted=1")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── 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 is the default: full-disk encryption, and in exchange the machine
|
||||||
|
# logs you straight into the desktop (the passphrase already gates access).
|
||||||
|
LUKS_PASSPHRASE=""
|
||||||
|
if [[ "$UNATTENDED" == "1" ]]; then
|
||||||
|
LUKS_PASSPHRASE="${NOMARCHY_LUKS_PASSPHRASE:-}"
|
||||||
|
elif gum confirm --default=yes "Encrypt the disk with LUKS? (default — also enables passwordless desktop login)"; 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 (desktop auto-login)" || echo none)"
|
||||||
|
|
||||||
|
# ─── Swap / hibernation ─────────────────────────────────────────────────
|
||||||
|
# A swapfile ≥ RAM on its own BTRFS subvolume makes hibernation possible;
|
||||||
|
# the resume offset is wired into the config below.
|
||||||
|
ram_gb=$(awk '/MemTotal/ {print int(($2 + 1048575) / 1048576)}' /proc/meminfo)
|
||||||
|
if [[ "$UNATTENDED" == "1" ]]; then
|
||||||
|
SWAP_GB="${NOMARCHY_SWAP_GB:-$ram_gb}"
|
||||||
|
else
|
||||||
|
SWAP_GB=$(gum input --value "$ram_gb" \
|
||||||
|
--placeholder "swap size in GiB (≥ RAM enables hibernation, 0 = none)")
|
||||||
|
fi
|
||||||
|
[[ "$SWAP_GB" =~ ^[0-9]+$ ]] || fail "Swap size must be a whole number of GiB."
|
||||||
|
info "Swap: $([[ "$SWAP_GB" == "0" ]] && echo none || echo "${SWAP_GB}G swapfile (hibernation-ready)")"
|
||||||
|
|
||||||
|
# ─── 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 + desktop auto-login" || echo none)" \
|
||||||
|
"Swap: $([[ "$SWAP_GB" == "0" ]] && echo none || echo "${SWAP_GB}G (hibernation)")" \
|
||||||
|
"User: $USERNAME" \
|
||||||
|
"Hostname: $HOSTNAME_" \
|
||||||
|
"Timezone: $TIMEZONE" \
|
||||||
|
"Hardware: ${HW_PROFILES[*]:-none}" \
|
||||||
|
"Snapshots: snapper timeline on /" \
|
||||||
|
"Source: nomarchy ${NOMARCHY_REV:0:12}${NOMARCHY_REV:+ }$([[ -z "${NOMARCHY_REV:-}" ]] && echo "(dirty tree) ")— pinned into the ISO, no network needed"
|
||||||
|
|
||||||
|
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
|
||||||
|
for part in "${drive}"?*; do
|
||||||
|
# unmatched glob stays literal; -b filters it out
|
||||||
|
[[ -b "$part" ]] || continue
|
||||||
|
wipefs -af "$part" >/dev/null
|
||||||
|
done
|
||||||
|
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" \
|
||||||
|
--argstr swapSize "${SWAP_GB}G" \
|
||||||
|
"$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"
|
||||||
|
|
||||||
|
# Hibernation plumbing: the swapfile's physical offset goes into the
|
||||||
|
# kernel cmdline. Deactivate swap first so nixos-generate-config doesn't
|
||||||
|
# also emit a swapDevices entry (we write our own, with resume wiring).
|
||||||
|
RESUME_CONFIG=""
|
||||||
|
if [[ "$SWAP_GB" != "0" ]]; then
|
||||||
|
swapoff -a 2>/dev/null || true
|
||||||
|
resume_offset=$(btrfs inspect-internal map-swapfile -r /mnt/swap/swapfile)
|
||||||
|
root_uuid=$(findmnt -no UUID /mnt)
|
||||||
|
RESUME_CONFIG=$(cat <<NIX
|
||||||
|
|
||||||
|
# Swapfile (hibernation-ready: resume points into it).
|
||||||
|
swapDevices = [{ device = "/swap/swapfile"; }];
|
||||||
|
boot.resumeDevice = "/dev/disk/by-uuid/$root_uuid";
|
||||||
|
boot.kernelParams = [ "resume_offset=$resume_offset" ];
|
||||||
|
NIX
|
||||||
|
)
|
||||||
|
success "Swapfile created (resume offset $resume_offset)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── 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
|
||||||
|
|
||||||
|
AUTOLOGIN_CONFIG=""
|
||||||
|
if [[ $WITH_LUKS == true ]]; then
|
||||||
|
AUTOLOGIN_CONFIG=$(cat <<NIX
|
||||||
|
|
||||||
|
# The LUKS passphrase already gates this machine — skip the second
|
||||||
|
# password prompt and boot straight into the desktop.
|
||||||
|
nomarchy.system.greeter.autoLogin = "$USERNAME";
|
||||||
|
NIX
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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";
|
||||||
|
};
|
||||||
|
$AUTOLOGIN_CONFIG$RESUME_CONFIG
|
||||||
|
# Hourly/daily BTRFS timeline snapshots + nixos-rebuild-snap.
|
||||||
|
nomarchy.system.snapper.enable = true;
|
||||||
|
|
||||||
|
system.stateVersion = "26.05";
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# The flake.lock: composed offline — nomarchy is path-locked to the very
|
||||||
|
# source the ISO carries (original stays the forge URL, so a later
|
||||||
|
# `nix flake update` on the installed machine re-resolves normally).
|
||||||
|
if ! python3 "$SHARE/compose-lock.py" "$SHARE/flake.lock" "$FLAKE_DIR/flake.lock" \
|
||||||
|
"$NOMARCHY_LOCKED_JSON" "$NOMARCHY_ORIGINAL_JSON"; then
|
||||||
|
warn "Offline lock composition failed — resolving over the 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
# The user must own their flake — libgit2 refuses repositories owned by
|
||||||
|
# someone else, which breaks `home-manager switch` (and theme switching)
|
||||||
|
# outright. The first normal NixOS user is always 1000:users(100); the
|
||||||
|
# account doesn't exist in the target yet, so numeric ids it is.
|
||||||
|
chown -R 1000:100 "$FLAKE_DIR"
|
||||||
|
|
||||||
|
# /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. Two steps because
|
||||||
|
# `flake archive --to` enforces signatures and locally-evaluated source
|
||||||
|
# paths have none; plain `nix copy` accepts --no-check-sigs.
|
||||||
|
info "Seeding flake inputs into the target store..."
|
||||||
|
# path: (not git+file) — the flake dir is owned by the target user and
|
||||||
|
# root's libgit2 refuses repositories owned by someone else.
|
||||||
|
flake_paths=$(nix --extra-experimental-features "nix-command flakes" \
|
||||||
|
flake archive --json "path:$FLAKE_DIR" \
|
||||||
|
| python3 -c '
|
||||||
|
import json, sys
|
||||||
|
def walk(node):
|
||||||
|
yield node["path"]
|
||||||
|
for child in node.get("inputs", {}).values():
|
||||||
|
yield from walk(child)
|
||||||
|
print("\n".join(walk(json.load(sys.stdin))))' || true)
|
||||||
|
if [[ -n "$flake_paths" ]]; then
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
nix --extra-experimental-features "nix-command flakes" \
|
||||||
|
copy --no-check-sigs --to "local?root=/mnt" $flake_paths \
|
||||||
|
|| warn "input seeding failed — first rebuild will need network."
|
||||||
|
else
|
||||||
|
warn "flake archive failed — first rebuild will need network."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Offline: sidestep substituter plumbing entirely — make every ISO store
|
||||||
|
# path valid in the target store up front. nixos-install's in-target
|
||||||
|
# build then finds all build tools locally and only the per-machine
|
||||||
|
# config derivations are built. (Two earlier attempts to route this
|
||||||
|
# through substituters — env config and the forwarded --substituters
|
||||||
|
# flag — still left the plan building gcc from source.)
|
||||||
|
if [[ ${#NIXOS_INSTALL_OPTS[@]} -gt 0 ]]; then
|
||||||
|
info "Copying the ISO store into the target (offline install)..."
|
||||||
|
nix --extra-experimental-features nix-command \
|
||||||
|
copy --all --no-check-sigs --to "local?root=/mnt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
nixos-install --no-root-passwd "${NIXOS_INSTALL_OPTS[@]}" --flake "path:$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"
|
||||||
|
# NOT /mnt/tmp: nixos-enter mounts a fresh tmpfs over /tmp inside the
|
||||||
|
# chroot, which silently vaporizes any script staged there (cost us a
|
||||||
|
# full verification round to find). /root persists into the chroot.
|
||||||
|
cat > /mnt/root/nomarchy-hm-activate.sh <<EOF
|
||||||
|
set -ex
|
||||||
|
exec > /var/log/nomarchy-hm-preactivate.log 2>&1
|
||||||
|
export PATH=/run/current-system/sw/bin:\$PATH
|
||||||
|
out=\$(nix --extra-experimental-features "nix-command flakes" \
|
||||||
|
build --no-link --print-out-paths \
|
||||||
|
--option substituters "" \
|
||||||
|
"path:/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
|
||||||
|
# activate's profile ops need store access; as the user that means a
|
||||||
|
# daemon, and the chroot has none — run one for the duration.
|
||||||
|
nix-daemon &
|
||||||
|
daemon_pid=\$!
|
||||||
|
trap 'kill \$daemon_pid 2>/dev/null || true' EXIT
|
||||||
|
sleep 2
|
||||||
|
# BACKUP_EXT: collisions can't abort the activation (a stray
|
||||||
|
# autogenerated config gets moved aside instead).
|
||||||
|
runuser -u "$USERNAME" -- bash -lc \
|
||||||
|
"USER=$USERNAME HOME=/home/$USERNAME NIX_REMOTE=daemon HOME_MANAGER_BACKUP_EXT=bak \$out/activate"
|
||||||
|
EOF
|
||||||
|
if nixos-enter --root /mnt -- bash /root/nomarchy-hm-activate.sh; then
|
||||||
|
success "Desktop pre-activated — first boot is fully themed"
|
||||||
|
else
|
||||||
|
warn "Desktop pre-activation failed (see /var/log/nomarchy-hm-preactivate.log"
|
||||||
|
warn "on the installed system); after first login run:"
|
||||||
|
warn " home-manager switch --flake ~/.nomarchy -b bak"
|
||||||
|
tail -n 5 /mnt/var/log/nomarchy-hm-preactivate.log 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -f /mnt/root/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
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
, stdenvNoCC
|
, stdenvNoCC
|
||||||
, python3
|
, python3
|
||||||
, makeWrapper
|
, makeWrapper
|
||||||
, swww
|
, awww
|
||||||
, libnotify
|
, libnotify
|
||||||
, git
|
, git
|
||||||
# Shipped theme presets, baked into the package as a fallback so
|
# Shipped theme presets, baked into the package as a fallback so
|
||||||
@@ -33,7 +33,7 @@ stdenvNoCC.mkDerivation {
|
|||||||
# Stdlib-only Python. home-manager is deliberately NOT wrapped in —
|
# Stdlib-only Python. home-manager is deliberately NOT wrapped in —
|
||||||
# the rebuild must use the user's own home-manager from their PATH.
|
# the rebuild must use the user's own home-manager from their PATH.
|
||||||
wrapProgram $out/bin/nomarchy-theme-sync \
|
wrapProgram $out/bin/nomarchy-theme-sync \
|
||||||
--prefix PATH : ${lib.makeBinPath [ swww libnotify git ]} \
|
--prefix PATH : ${lib.makeBinPath [ awww libnotify git ]} \
|
||||||
${lib.optionalString (themesDir != null)
|
${lib.optionalString (themesDir != null)
|
||||||
"--set NOMARCHY_DEFAULT_THEMES $out/share/nomarchy/themes"}
|
"--set NOMARCHY_DEFAULT_THEMES $out/share/nomarchy/themes"}
|
||||||
|
|
||||||
|
|||||||
@@ -168,16 +168,18 @@ def apply_wallpaper(state: dict, wait: bool = False) -> None:
|
|||||||
if wallpaper is None:
|
if wallpaper is None:
|
||||||
log(f"no wallpaper for theme '{state.get('slug', '?')}', skipping")
|
log(f"no wallpaper for theme '{state.get('slug', '?')}', skipping")
|
||||||
return
|
return
|
||||||
if shutil.which("swww") is None:
|
# nixpkgs renamed swww to awww (CLI-compatible fork); accept either.
|
||||||
log("swww not found, skipping wallpaper")
|
swww = shutil.which("awww") or shutil.which("swww")
|
||||||
|
if swww is None:
|
||||||
|
log("awww/swww not found, skipping wallpaper")
|
||||||
return
|
return
|
||||||
# At session start swww-daemon may still be coming up.
|
# At session start the daemon may still be coming up.
|
||||||
for _ in range(10 if wait else 1):
|
for _ in range(10 if wait else 1):
|
||||||
if subprocess.run(["swww", "query"], capture_output=True).returncode == 0:
|
if subprocess.run([swww, "query"], capture_output=True).returncode == 0:
|
||||||
break
|
break
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["swww", "img", str(wallpaper),
|
[swww, "img", str(wallpaper),
|
||||||
"--transition-type", "grow",
|
"--transition-type", "grow",
|
||||||
"--transition-pos", "center",
|
"--transition-pos", "center",
|
||||||
"--transition-duration", "1",
|
"--transition-duration", "1",
|
||||||
@@ -185,7 +187,7 @@ def apply_wallpaper(state: dict, wait: bool = False) -> None:
|
|||||||
capture_output=True,
|
capture_output=True,
|
||||||
)
|
)
|
||||||
log(f"wallpaper: {wallpaper.name}" if result.returncode == 0
|
log(f"wallpaper: {wallpaper.name}" if result.returncode == 0
|
||||||
else "wallpaper: swww failed (daemon not running?)")
|
else "wallpaper: awww failed (daemon not running?)")
|
||||||
|
|
||||||
|
|
||||||
# ─── Commands ─────────────────────────────────────────────────────────────
|
# ─── Commands ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
# My Nomarchy machine
|
# My Nomarchy machine
|
||||||
|
|
||||||
1. `nixos-generate-config --show-hardware-config > hardware-configuration.nix`
|
1. Overwrite the placeholder hardware config with the real one:
|
||||||
2. Edit `flake.nix` (Nomarchy repo URL, username), `system.nix` (hostname,
|
`nixos-generate-config --show-hardware-config > hardware-configuration.nix`
|
||||||
user), `home.nix` (your packages).
|
2. Set `flake.nix` up **once** (Nomarchy repo URL, your username, optionally
|
||||||
|
a `hardwareProfile` from <https://github.com/NixOS/nixos-hardware> — an
|
||||||
|
unknown name fails the rebuild with suggestions). After that the flake is
|
||||||
|
never touched again: your machine lives in `system.nix` (hostname,
|
||||||
|
services — the login user is created from `username` automatically) and
|
||||||
|
`home.nix` (your packages).
|
||||||
3. `git init && git add -A` — flakes only see tracked files, including
|
3. `git init && git add -A` — flakes only see tracked files, including
|
||||||
`theme-state.json`.
|
`theme-state.json`.
|
||||||
4. System: `sudo nixos-rebuild switch --flake .#default`
|
4. System: `sudo nixos-rebuild switch --flake .#default`
|
||||||
|
|||||||
@@ -1,53 +1,20 @@
|
|||||||
{
|
{
|
||||||
description = "My Nomarchy machine";
|
description = "My Nomarchy machine";
|
||||||
|
|
||||||
inputs = {
|
# The only input. nixpkgs, home-manager etc. come pinned through it —
|
||||||
# Pin Nomarchy; nixpkgs and home-manager follow whatever it ships.
|
# tested together upstream. This file is written once (by you or the
|
||||||
nomarchy.url = "github:YOUR-USER/nomarchy"; # <- point at the distro repo
|
# installer) and never hand-edited afterwards; your machine lives in
|
||||||
nixpkgs.follows = "nomarchy/nixpkgs";
|
# system.nix and home.nix.
|
||||||
home-manager.follows = "nomarchy/home-manager";
|
inputs.nomarchy.url = "github:YOUR-USER/nomarchy"; # <- the distro repo
|
||||||
};
|
|
||||||
|
|
||||||
outputs = { self, nomarchy, nixpkgs, home-manager, ... }:
|
outputs = { nomarchy, ... }:
|
||||||
let
|
nomarchy.lib.mkFlake {
|
||||||
system = "x86_64-linux";
|
src = ./.;
|
||||||
username = "me"; # <- your login name
|
username = "me"; # <- your login name
|
||||||
|
|
||||||
pkgs = import nixpkgs {
|
# Optional: a nixos-hardware module name for your machine, e.g.
|
||||||
inherit system;
|
# hardwareProfile = "framework-13-7040-amd";
|
||||||
overlays = [ nomarchy.overlays.default ];
|
# Names: https://github.com/NixOS/nixos-hardware
|
||||||
};
|
# (the future installer fills this in automatically from DMI data)
|
||||||
in
|
|
||||||
{
|
|
||||||
# System layer — rebuilt rarely:
|
|
||||||
# sudo nixos-rebuild switch --flake .#default
|
|
||||||
nixosConfigurations.default = nixpkgs.lib.nixosSystem {
|
|
||||||
inherit system;
|
|
||||||
modules = [
|
|
||||||
nomarchy.nixosModules.nomarchy # the distro
|
|
||||||
./hardware-configuration.nix # from `nixos-generate-config`
|
|
||||||
./system.nix # your machine
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
# Desktop layer — rebuilt on every theme change, no sudo:
|
|
||||||
# home-manager switch --flake .#me
|
|
||||||
# (`nomarchy-theme-sync apply <theme>` runs this for you.)
|
|
||||||
homeConfigurations.${username} = home-manager.lib.homeManagerConfiguration {
|
|
||||||
inherit pkgs;
|
|
||||||
modules = [
|
|
||||||
nomarchy.homeModules.nomarchy
|
|
||||||
./home.nix
|
|
||||||
{
|
|
||||||
# Your theme state — written by nomarchy-theme-sync, baked
|
|
||||||
# in on every switch. Keep it git-tracked.
|
|
||||||
nomarchy.stateFile = ./theme-state.json;
|
|
||||||
home = {
|
|
||||||
inherit username;
|
|
||||||
homeDirectory = "/home/${username}";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
15
templates/downstream/hardware-configuration.nix
Normal file
15
templates/downstream/hardware-configuration.nix
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# PLACEHOLDER — replace with the output of `nixos-generate-config` on the
|
||||||
|
# target machine. The mkDefault values below only exist so the flake
|
||||||
|
# evaluates and `nix flake check` passes before first install.
|
||||||
|
{ lib, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
boot.initrd.availableKernelModules = lib.mkDefault [ "xhci_pci" "ahci" "nvme" "usbhid" "sd_mod" ];
|
||||||
|
|
||||||
|
fileSystems."/" = lib.mkDefault {
|
||||||
|
device = "/dev/disk/by-label/nixos";
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
# The distro itself comes from nomarchy.nixosModules.nomarchy — override
|
# The distro itself comes from nomarchy.nixosModules.nomarchy — override
|
||||||
# any of its defaults here with plain NixOS options (they use mkDefault),
|
# any of its defaults here with plain NixOS options (they use mkDefault),
|
||||||
# or via the nomarchy.system.* toggles.
|
# or via the nomarchy.system.* toggles.
|
||||||
{ pkgs, ... }:
|
{ pkgs, username, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
boot.loader.systemd-boot.enable = true;
|
boot.loader.systemd-boot.enable = true;
|
||||||
@@ -12,7 +12,8 @@
|
|||||||
time.timeZone = "UTC";
|
time.timeZone = "UTC";
|
||||||
i18n.defaultLocale = "en_US.UTF-8";
|
i18n.defaultLocale = "en_US.UTF-8";
|
||||||
|
|
||||||
users.users.me = { # <- keep in sync with `username` in flake.nix
|
# Your login user — `username` flows in from flake.nix automatically.
|
||||||
|
users.users.${username} = {
|
||||||
isNormalUser = true;
|
isNormalUser = true;
|
||||||
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
|
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
|
||||||
};
|
};
|
||||||
|
|||||||
151
tools/test-install.sh
Executable file
151
tools/test-install.sh
Executable file
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# End-to-end regression test for nomarchy-install: build the live ISO,
|
||||||
|
# run a fully OFFLINE unattended install into a fresh VM disk, then boot
|
||||||
|
# the installed system and capture what the first boot looks like.
|
||||||
|
#
|
||||||
|
# This automates the verification protocol from docs/TESTING.md §4 that
|
||||||
|
# debugged the installer in the first place. It asserts the machine-
|
||||||
|
# checkable parts (installer reaches poweroff; installed disk boots);
|
||||||
|
# the visual part — a fully themed desktop, no autogenerated-config
|
||||||
|
# banner — lands as screenshots for a human (or agent) to inspect:
|
||||||
|
#
|
||||||
|
# $NOMARCHY_VM_DIR/install-typed.png the typed command, pre-enter
|
||||||
|
# $NOMARCHY_VM_DIR/luks-prompt.png initrd passphrase prompt
|
||||||
|
# $NOMARCHY_VM_DIR/first-boot.png THE result: themed desktop
|
||||||
|
#
|
||||||
|
# Requirements: KVM, qemu, OVMF, dosfstools (mkfs.vfat), mtools (mcopy),
|
||||||
|
# python3. Tunables (env):
|
||||||
|
# NOMARCHY_VM_DIR scratch + screenshots (default /tmp/nomarchy-vm)
|
||||||
|
# NOMARCHY_TARGET_DIR where the 25G target.img lives — must NOT be
|
||||||
|
# tmpfs (default ~/.cache/nomarchy-vm)
|
||||||
|
# NOMARCHY_BOOT_WAIT seconds to wait for the live desktop (default 480)
|
||||||
|
# NOMARCHY_INSTALL_TIMEOUT seconds before declaring a hang (default 2700)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
VM_DIR="${NOMARCHY_VM_DIR:-/tmp/nomarchy-vm}"
|
||||||
|
TARGET_DIR="${NOMARCHY_TARGET_DIR:-$HOME/.cache/nomarchy-vm}"
|
||||||
|
BOOT_WAIT="${NOMARCHY_BOOT_WAIT:-480}"
|
||||||
|
INSTALL_TIMEOUT="${NOMARCHY_INSTALL_TIMEOUT:-2700}"
|
||||||
|
export NOMARCHY_QMP_SOCK="$VM_DIR/qmp.sock"
|
||||||
|
QMP=tools/vm/qmp.py
|
||||||
|
SHOT=tools/vm/vncshot.py
|
||||||
|
VNC_DISPLAY=17 # port 5917
|
||||||
|
|
||||||
|
for tool in qemu-system-x86_64 mkfs.vfat mcopy python3; do
|
||||||
|
command -v "$tool" >/dev/null || {
|
||||||
|
echo "missing $tool — try: nix shell nixpkgs#qemu nixpkgs#dosfstools nixpkgs#mtools" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
[ -r /dev/kvm ] || { echo "no KVM — this test is too slow without it" >&2; exit 1; }
|
||||||
|
|
||||||
|
mkdir -p "$VM_DIR" "$TARGET_DIR"
|
||||||
|
if df --output=fstype "$TARGET_DIR" | tail -1 | grep -q tmpfs; then
|
||||||
|
echo "NOMARCHY_TARGET_DIR=$TARGET_DIR is tmpfs — the install writes ~10G; pick a real disk" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
OVMF=""
|
||||||
|
for c in /run/current-system/sw/share/qemu/edk2-x86_64-code.fd \
|
||||||
|
/run/current-system/sw/share/OVMF/OVMF_CODE.fd \
|
||||||
|
/usr/share/OVMF/OVMF_CODE.fd; do
|
||||||
|
[ -f "$c" ] && { OVMF="$c"; break; }
|
||||||
|
done
|
||||||
|
[ -n "$OVMF" ] || { echo "no OVMF firmware found" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "==> Building the live ISO..."
|
||||||
|
nix build .#nixosConfigurations.nomarchy-live.config.system.build.isoImage
|
||||||
|
ISO=$(ls -1 result/iso/*.iso | head -n1)
|
||||||
|
|
||||||
|
echo "==> Preparing disks..."
|
||||||
|
# Unattended config rides in on a tiny vfat disk: the installer test
|
||||||
|
# can't reliably TYPE a 200-char env line into the guest (keystrokes
|
||||||
|
# get dropped), but one short mount-and-run command works.
|
||||||
|
cat > "$VM_DIR/go.sh" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
export NOMARCHY_UNATTENDED=1
|
||||||
|
export NOMARCHY_DISK=/dev/vda
|
||||||
|
export NOMARCHY_USERNAME=ada
|
||||||
|
export NOMARCHY_PASSWORD=nomarchy
|
||||||
|
export NOMARCHY_HOSTNAME=testbox
|
||||||
|
export NOMARCHY_LUKS_PASSPHRASE=testtest1
|
||||||
|
export NOMARCHY_SWAP_GB=2
|
||||||
|
export NOMARCHY_FINISH=poweroff
|
||||||
|
nomarchy-install 2>&1 | tee /tmp/install.log
|
||||||
|
EOF
|
||||||
|
rm -f "$VM_DIR/config.img"
|
||||||
|
truncate -s 8M "$VM_DIR/config.img"
|
||||||
|
mkfs.vfat "$VM_DIR/config.img" >/dev/null
|
||||||
|
mcopy -i "$VM_DIR/config.img" "$VM_DIR/go.sh" ::go.sh
|
||||||
|
|
||||||
|
rm -f "$TARGET_DIR/target.img" "$NOMARCHY_QMP_SOCK"
|
||||||
|
qemu-img create -f raw "$TARGET_DIR/target.img" 25G >/dev/null
|
||||||
|
|
||||||
|
qemu_base=(
|
||||||
|
qemu-system-x86_64
|
||||||
|
-enable-kvm -cpu host -m 6144 -smp 2
|
||||||
|
-drive "if=pflash,format=raw,readonly=on,file=$OVMF"
|
||||||
|
-device virtio-vga-gl -display egl-headless
|
||||||
|
-vnc "127.0.0.1:$VNC_DISPLAY"
|
||||||
|
-device virtio-net-pci,netdev=n0 -netdev user,id=n0,restrict=on # OFFLINE
|
||||||
|
-qmp "unix:$NOMARCHY_QMP_SOCK,server,nowait"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "==> Phase 1: installing (offline) — this takes ~20-30 min..."
|
||||||
|
"${qemu_base[@]}" \
|
||||||
|
-drive "file=$TARGET_DIR/target.img,if=virtio,format=raw" \
|
||||||
|
-drive "file=$VM_DIR/config.img,if=virtio,format=raw,readonly=on" \
|
||||||
|
-cdrom "$ISO" -boot d \
|
||||||
|
>"$VM_DIR/qemu-install.log" 2>&1 &
|
||||||
|
QPID=$!
|
||||||
|
trap 'kill $QPID 2>/dev/null || true' EXIT
|
||||||
|
|
||||||
|
until [ -S "$NOMARCHY_QMP_SOCK" ]; do sleep 2; done
|
||||||
|
echo " waiting ${BOOT_WAIT}s for the live desktop..."
|
||||||
|
sleep "$BOOT_WAIT"
|
||||||
|
python3 "$QMP" key meta_l-ret
|
||||||
|
sleep 8
|
||||||
|
python3 "$QMP" type 'sudo bash -c "mkdir -p /m; mount /dev/vdb /m; bash /m/go.sh"'
|
||||||
|
sleep 2
|
||||||
|
python3 "$SHOT" "$VM_DIR/install-typed.png" 127.0.0.1 "59$VNC_DISPLAY"
|
||||||
|
python3 "$QMP" key ret
|
||||||
|
echo " install running (typed command captured in install-typed.png)..."
|
||||||
|
|
||||||
|
waited=0
|
||||||
|
while kill -0 $QPID 2>/dev/null; do
|
||||||
|
if (( waited >= INSTALL_TIMEOUT )); then
|
||||||
|
python3 "$SHOT" "$VM_DIR/install-hung.png" 127.0.0.1 "59$VNC_DISPLAY" || true
|
||||||
|
echo "FAIL: installer did not power off within ${INSTALL_TIMEOUT}s (see install-hung.png)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 20; waited=$((waited + 20))
|
||||||
|
done
|
||||||
|
trap - EXIT
|
||||||
|
echo " VM powered off after ~${waited}s — install completed."
|
||||||
|
|
||||||
|
echo "==> Phase 2: first boot of the installed system..."
|
||||||
|
rm -f "$NOMARCHY_QMP_SOCK"
|
||||||
|
"${qemu_base[@]}" \
|
||||||
|
-drive "file=$TARGET_DIR/target.img,if=virtio,format=raw" \
|
||||||
|
>"$VM_DIR/qemu-boot.log" 2>&1 &
|
||||||
|
QPID=$!
|
||||||
|
trap 'kill $QPID 2>/dev/null || true' EXIT
|
||||||
|
|
||||||
|
until [ -S "$NOMARCHY_QMP_SOCK" ]; do sleep 2; done
|
||||||
|
sleep 40
|
||||||
|
python3 "$SHOT" "$VM_DIR/luks-prompt.png" 127.0.0.1 "59$VNC_DISPLAY"
|
||||||
|
python3 "$QMP" type 'testtest1'
|
||||||
|
python3 "$QMP" key ret
|
||||||
|
sleep 150
|
||||||
|
python3 "$SHOT" "$VM_DIR/first-boot.png" 127.0.0.1 "59$VNC_DISPLAY"
|
||||||
|
python3 "$QMP" quit || true
|
||||||
|
trap - EXIT
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "PASS (machine-checkable part): install powered off, installed disk boots."
|
||||||
|
echo "Now INSPECT $VM_DIR/first-boot.png:"
|
||||||
|
echo " expected: themed desktop (wallpaper + waybar), NO red banner, no login prompt"
|
||||||
|
echo " if it shows the Hyprland water-drop or an 'autogenerated config' banner,"
|
||||||
|
echo " log in on tty2 (ada/nomarchy) and read /var/log/nomarchy-hm-preactivate.log"
|
||||||
74
tools/vm/gap-analysis.py
Executable file
74
tools/vm/gap-analysis.py
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Which store paths would an offline nixos-install need that the ISO lacks?
|
||||||
|
|
||||||
|
Walk the target drv graph: an output that's PRESENT needs nothing; a
|
||||||
|
missing output forces building its drv, which needs all its input
|
||||||
|
sources present and all referenced input outputs (recursively).
|
||||||
|
|
||||||
|
This is the maintainer tool that converged the offline pin set in
|
||||||
|
flake.nix (extraDependencies): when an offline install builds from
|
||||||
|
source, run this instead of bisecting the VM. Healthy output is a few
|
||||||
|
dozen per-machine config drvs (etc, initrd-*, unit-*, …) and zero
|
||||||
|
[FETCH/NETWORK] leaves; any real package name here needs pinning.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# 1. the present set = everything the ISO carries:
|
||||||
|
live=$(nix build --no-link --print-out-paths \
|
||||||
|
.#nixosConfigurations.nomarchy-live.config.system.build.toplevel)
|
||||||
|
nix path-info -r "$live" | sort -u > /tmp/present.txt
|
||||||
|
# 2. instantiate an install-shaped toplevel for a FOREIGN
|
||||||
|
# username/hostname/uuids (see docs/TESTING.md) and:
|
||||||
|
gap-analysis.py <target.drv> /tmp/present.txt
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
target_drv, present_file = sys.argv[1], sys.argv[2]
|
||||||
|
present = set(open(present_file).read().split())
|
||||||
|
|
||||||
|
graph = json.loads(subprocess.check_output(
|
||||||
|
["nix", "derivation", "show", "-r", target_drv + "^*"]))
|
||||||
|
|
||||||
|
must_build = set()
|
||||||
|
missing_srcs = set()
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
|
||||||
|
def outputs_of(drv_name):
|
||||||
|
return [o["path"] for o in graph[drv_name]["outputs"].values()]
|
||||||
|
|
||||||
|
|
||||||
|
def need_drv(drv_name):
|
||||||
|
if drv_name in visited:
|
||||||
|
return
|
||||||
|
visited.add(drv_name)
|
||||||
|
must_build.add(drv_name)
|
||||||
|
node = graph[drv_name]
|
||||||
|
for src in node.get("inputSrcs", []):
|
||||||
|
if src not in present:
|
||||||
|
missing_srcs.add(src)
|
||||||
|
for dep_drv, want in node.get("inputDrvs", {}).items():
|
||||||
|
dep_outputs = graph.get(dep_drv)
|
||||||
|
if dep_outputs is None:
|
||||||
|
continue
|
||||||
|
wanted = want.get("outputs", []) if isinstance(want, dict) else want
|
||||||
|
for out_name in wanted:
|
||||||
|
path = graph[dep_drv]["outputs"][out_name]["path"]
|
||||||
|
if path not in present:
|
||||||
|
need_drv(dep_drv)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
# Roots: the target's own outputs are by definition "to build".
|
||||||
|
need_drv(target_drv)
|
||||||
|
|
||||||
|
print(f"drvs that must be built offline: {len(must_build)}")
|
||||||
|
for d in sorted(must_build):
|
||||||
|
name = d.split("-", 1)[1] if "-" in d else d
|
||||||
|
builder = graph[d].get("builder", "")
|
||||||
|
fetch = " [FETCH/NETWORK]" if graph[d].get("env", {}).get("urls") or graph[d].get("env", {}).get("url") else ""
|
||||||
|
print(f" {name}{fetch}")
|
||||||
|
print(f"missing input sources: {len(missing_srcs)}")
|
||||||
|
for s in sorted(missing_srcs):
|
||||||
|
print(f" SRC {s}")
|
||||||
105
tools/vm/qmp.py
Executable file
105
tools/vm/qmp.py
Executable file
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Minimal QMP driver for the Nomarchy test VMs.
|
||||||
|
|
||||||
|
Drives a QEMU instance through its QMP socket: inject keystrokes, type
|
||||||
|
text, take screendumps, power off. Used by tools/test-install.sh; also
|
||||||
|
handy interactively while debugging a VM that has no SSH.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
qmp.py key <combo> # e.g. meta_l-ret, ctrl-alt-f2, ret
|
||||||
|
qmp.py type <text...> # types literal text (US layout)
|
||||||
|
qmp.py screenshot <out.png> # NOTE: with virtio-vga-gl this fails
|
||||||
|
# ("no surface") once the guest uses GL —
|
||||||
|
# use vncshot.py instead for desktops
|
||||||
|
qmp.py quit # hard power-off
|
||||||
|
|
||||||
|
The socket path comes from $NOMARCHY_QMP_SOCK
|
||||||
|
(default /tmp/nomarchy-vm/qmp.sock).
|
||||||
|
|
||||||
|
Keystroke pacing: the guest drops keys when busy (especially during
|
||||||
|
boot). $NOMARCHY_TYPE_DELAY (seconds between keys, default 0.08) trades
|
||||||
|
speed for reliability; verify long typed lines with a screenshot before
|
||||||
|
pressing enter.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
SOCK = os.environ.get("NOMARCHY_QMP_SOCK", "/tmp/nomarchy-vm/qmp.sock")
|
||||||
|
TYPE_DELAY = float(os.environ.get("NOMARCHY_TYPE_DELAY", "0.08"))
|
||||||
|
|
||||||
|
CHAR_KEYS = {
|
||||||
|
" ": "spc", "-": "minus", "=": "equal", "[": "bracket_left",
|
||||||
|
"]": "bracket_right", ";": "semicolon", "'": "apostrophe",
|
||||||
|
"`": "grave_accent", "\\": "backslash", ",": "comma", ".": "dot",
|
||||||
|
"/": "slash", "\n": "ret", "\t": "tab",
|
||||||
|
}
|
||||||
|
SHIFT_CHARS = {
|
||||||
|
"!": "1", "@": "2", "#": "3", "$": "4", "%": "5", "^": "6",
|
||||||
|
"&": "7", "*": "8", "(": "9", ")": "0", "_": "minus", "+": "equal",
|
||||||
|
"{": "bracket_left", "}": "bracket_right", ":": "semicolon",
|
||||||
|
'"': "apostrophe", "~": "grave_accent", "|": "backslash",
|
||||||
|
"<": "comma", ">": "dot", "?": "slash",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class QMP:
|
||||||
|
def __init__(self):
|
||||||
|
self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
self.s.connect(SOCK)
|
||||||
|
self.f = self.s.makefile("rw")
|
||||||
|
self._read() # greeting
|
||||||
|
self.cmd("qmp_capabilities")
|
||||||
|
|
||||||
|
def _read(self):
|
||||||
|
while True:
|
||||||
|
msg = json.loads(self.f.readline())
|
||||||
|
if "event" in msg:
|
||||||
|
continue
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def cmd(self, name, **args):
|
||||||
|
self.f.write(json.dumps({"execute": name, "arguments": args}) + "\n")
|
||||||
|
self.f.flush()
|
||||||
|
resp = self._read()
|
||||||
|
if "error" in resp:
|
||||||
|
raise RuntimeError(f"{name}: {resp['error']}")
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def send_keys(self, names, hold=40):
|
||||||
|
keys = [{"type": "qcode", "data": n} for n in names]
|
||||||
|
self.cmd("send-key", keys=keys, **{"hold-time": hold})
|
||||||
|
|
||||||
|
def type_text(self, text):
|
||||||
|
for ch in text:
|
||||||
|
if ch.isalpha() and ch.lower() != ch:
|
||||||
|
self.send_keys(["shift", ch.lower()])
|
||||||
|
elif ch in SHIFT_CHARS:
|
||||||
|
self.send_keys(["shift", SHIFT_CHARS[ch]])
|
||||||
|
elif ch in CHAR_KEYS:
|
||||||
|
self.send_keys([CHAR_KEYS[ch]])
|
||||||
|
else:
|
||||||
|
self.send_keys([ch])
|
||||||
|
time.sleep(TYPE_DELAY)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
q = QMP()
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
if cmd == "screenshot":
|
||||||
|
q.cmd("screendump", filename=sys.argv[2], format="png")
|
||||||
|
elif cmd == "key":
|
||||||
|
q.send_keys(sys.argv[2].split("-"))
|
||||||
|
elif cmd == "type":
|
||||||
|
q.type_text(" ".join(sys.argv[2:]))
|
||||||
|
elif cmd == "quit":
|
||||||
|
q.f.write(json.dumps({"execute": "quit"}) + "\n")
|
||||||
|
q.f.flush()
|
||||||
|
else:
|
||||||
|
sys.exit(f"unknown command {cmd}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
104
tools/vm/vncshot.py
Executable file
104
tools/vm/vncshot.py
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Grab one full framebuffer from a no-auth VNC server and save as PNG.
|
||||||
|
|
||||||
|
Why VNC and not QMP screendump: with virtio-vga-gl the guest renders
|
||||||
|
into a GL scanout and screendump fails with "no surface"; QEMU's VNC
|
||||||
|
server reads the GL framebuffer back, so this always works. Pair the VM
|
||||||
|
with `-display egl-headless -vnc 127.0.0.1:17`.
|
||||||
|
|
||||||
|
Stdlib only (manual RFB handshake + raw-encoding framebuffer + PNG
|
||||||
|
writer) so it runs anywhere python3 exists.
|
||||||
|
|
||||||
|
Usage: vncshot.py <out.png> [host] [port] # default 127.0.0.1:5917
|
||||||
|
"""
|
||||||
|
import socket, struct, sys, zlib
|
||||||
|
|
||||||
|
def recv_exact(s, n):
|
||||||
|
buf = b""
|
||||||
|
while len(buf) < n:
|
||||||
|
chunk = s.recv(n - len(buf))
|
||||||
|
if not chunk:
|
||||||
|
raise EOFError("server closed")
|
||||||
|
buf += chunk
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def png_write(path, w, h, rgb):
|
||||||
|
def chunk(tag, data):
|
||||||
|
c = struct.pack(">I", len(data)) + tag + data
|
||||||
|
return c + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
|
||||||
|
raw = b"".join(b"\x00" + rgb[y * w * 3:(y + 1) * w * 3] for y in range(h))
|
||||||
|
out = (b"\x89PNG\r\n\x1a\n"
|
||||||
|
+ chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
|
||||||
|
+ chunk(b"IDAT", zlib.compress(raw, 6))
|
||||||
|
+ chunk(b"IEND", b""))
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(out)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
out = sys.argv[1]
|
||||||
|
host = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1"
|
||||||
|
port = int(sys.argv[3]) if len(sys.argv) > 3 else 5917
|
||||||
|
s = socket.create_connection((host, port), timeout=30)
|
||||||
|
s.settimeout(30)
|
||||||
|
|
||||||
|
ver = recv_exact(s, 12) # "RFB 003.008\n"
|
||||||
|
s.sendall(b"RFB 003.008\n")
|
||||||
|
ntypes = recv_exact(s, 1)[0]
|
||||||
|
types = recv_exact(s, ntypes)
|
||||||
|
if 1 not in types:
|
||||||
|
sys.exit(f"no 'None' security type offered: {list(types)}")
|
||||||
|
s.sendall(b"\x01")
|
||||||
|
res = struct.unpack(">I", recv_exact(s, 4))[0]
|
||||||
|
if res != 0:
|
||||||
|
sys.exit("security handshake failed")
|
||||||
|
s.sendall(b"\x01") # ClientInit: shared
|
||||||
|
w, h = struct.unpack(">HH", recv_exact(s, 4))
|
||||||
|
recv_exact(s, 16) # server pixel format (ignored)
|
||||||
|
namelen = struct.unpack(">I", recv_exact(s, 4))[0]
|
||||||
|
recv_exact(s, namelen)
|
||||||
|
|
||||||
|
# SetPixelFormat: 32bpp truecolor, shifts r=16 g=8 b=0 (little-endian BGRX)
|
||||||
|
pf = struct.pack(">BBBBHHHBBBxxx", 32, 24, 0, 1, 255, 255, 255, 16, 8, 0)
|
||||||
|
s.sendall(b"\x00\x00\x00\x00" + pf)
|
||||||
|
s.sendall(struct.pack(">BxH i", 2, 1, 0)) # SetEncodings: Raw only
|
||||||
|
s.sendall(struct.pack(">BBHHHH", 3, 0, 0, 0, w, h)) # full update request
|
||||||
|
|
||||||
|
fb = bytearray(w * h * 4)
|
||||||
|
got = False
|
||||||
|
while not got:
|
||||||
|
mtype = recv_exact(s, 1)[0]
|
||||||
|
if mtype == 0: # FramebufferUpdate
|
||||||
|
recv_exact(s, 1)
|
||||||
|
nrects = struct.unpack(">H", recv_exact(s, 2))[0]
|
||||||
|
for _ in range(nrects):
|
||||||
|
x, y, rw, rh = struct.unpack(">HHHH", recv_exact(s, 8))
|
||||||
|
enc = struct.unpack(">i", recv_exact(s, 4))[0]
|
||||||
|
if enc == 0:
|
||||||
|
data = recv_exact(s, rw * rh * 4)
|
||||||
|
for row in range(rh):
|
||||||
|
off = ((y + row) * w + x) * 4
|
||||||
|
fb[off:off + rw * 4] = data[row * rw * 4:(row + 1) * rw * 4]
|
||||||
|
elif enc == -224: # LastRect
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
sys.exit(f"unsupported encoding {enc}")
|
||||||
|
got = True
|
||||||
|
elif mtype == 2: # Bell
|
||||||
|
pass
|
||||||
|
elif mtype == 3: # ServerCutText
|
||||||
|
recv_exact(s, 3)
|
||||||
|
ln = struct.unpack(">I", recv_exact(s, 4))[0]
|
||||||
|
recv_exact(s, ln)
|
||||||
|
else:
|
||||||
|
sys.exit(f"unexpected server message {mtype}")
|
||||||
|
|
||||||
|
rgb = bytearray(w * h * 3)
|
||||||
|
for i in range(w * h):
|
||||||
|
rgb[i * 3 + 0] = fb[i * 4 + 2]
|
||||||
|
rgb[i * 3 + 1] = fb[i * 4 + 1]
|
||||||
|
rgb[i * 3 + 2] = fb[i * 4 + 0]
|
||||||
|
png_write(out, w, h, bytes(rgb))
|
||||||
|
print(f"saved {out} ({w}x{h})")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user