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 │
|
||||
└───────────────────────────────────┬──────────────────────────────────────┘
|
||||
▼
|
||||
wallpaper via swww (the one runtime piece:
|
||||
wallpaper via awww (the one runtime piece:
|
||||
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)
|
||||
├── lib.nix # nomarchy.lib.mkFlake — one-call downstream wrapper
|
||||
├── theme-state.json # ★ THE single source of truth (git-tracked!)
|
||||
├── themes/ # 21 presets: <slug>.json + optional <slug>/ assets
|
||||
│ ├── nord.json # palette (required, works alone)
|
||||
@@ -57,12 +58,17 @@ Flat on purpose. Two module trees, one options file each, no hidden layers.
|
||||
├── hosts/
|
||||
│ ├── default/ # reference machine (thin: boot, user, hostname)
|
||||
│ └── live.nix # bootable live ISO (try the distro, no install)
|
||||
├── pkgs/nomarchy-theme-sync/ # state writer + rebuild dispatcher (Python)
|
||||
├── pkgs/
|
||||
│ ├── nomarchy-theme-sync/ # state writer + rebuild dispatcher (Python)
|
||||
│ └── nomarchy-install/ # live-ISO installer (gum + disko + mkFlake)
|
||||
├── templates/downstream/ # `nix flake init -t` starter for users
|
||||
├── docs/TESTING.md # how to verify changes (incl. AI-agent rules)
|
||||
└── tools/ # maintainer-only
|
||||
├── 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),
|
||||
@@ -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
|
||||
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)
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
You own three files: `system.nix`, `home.nix`, and **your own
|
||||
`theme-state.json`**. Two deliberately separate rebuild paths:
|
||||
You own two files day-to-day: `system.nix` and `home.nix` (plus
|
||||
`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
|
||||
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
|
||||
re-reads `style.css` when the symlink flips.
|
||||
|
||||
The **wallpaper** is the one runtime piece (swww is imperative; nothing in
|
||||
Nix consumes the path): applied at session start and after every switch via
|
||||
a tiny activation hook, cycled instantly with `bg next`.
|
||||
The **wallpaper** is the one runtime piece (awww — nixpkgs' swww — is
|
||||
imperative; nothing in Nix consumes the path): applied at session start and
|
||||
after every switch via a tiny activation hook, cycled instantly with
|
||||
`bg next`.
|
||||
|
||||
### 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
|
||||
variants if still needed
|
||||
- Plymouth + SDDM/greeter theming from the same JSON
|
||||
- Interactive installer on the live ISO (disko BTRFS+LUKS, hardware
|
||||
profiles) — port from old_distro
|
||||
- Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy
|
||||
boot (v1 `nomarchy-install` is single-disk UEFI — see `pkgs/nomarchy-install`)
|
||||
- `hyprlock`/`hypridle`, swayosd, launch-or-focus UX scripts
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -51,7 +53,7 @@ Work through these in order; each one exercises a different layer.
|
||||
|
||||
| # | 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 |
|
||||
| 3 | `SUPER+Return` opens Ghostty with Tokyo Night colors | terminal default, ANSI palette |
|
||||
| 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
|
||||
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
|
||||
`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
|
||||
disk. Subsequent switches reuse the eval cache.
|
||||
|
||||
## 5. When something fails
|
||||
## 6. When something fails
|
||||
|
||||
- Session/login problems: `journalctl -b -u greetd`, then `journalctl
|
||||
--user -b`.
|
||||
|
||||
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
|
||||
}
|
||||
196
flake.nix
196
flake.nix
@@ -14,9 +14,17 @@
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
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
|
||||
system = "x86_64-linux";
|
||||
username = "nomarchy"; # reference host only; downstream picks its own
|
||||
@@ -25,6 +33,31 @@
|
||||
inherit system;
|
||||
overlays = [ self.overlays.default ];
|
||||
};
|
||||
|
||||
# The guided installer — live-ISO only (not in the overlay): it bakes
|
||||
# in the downstream template, this checkout's flake.lock, and the
|
||||
# source identity of this very revision so installs work offline.
|
||||
gitUrl = "https://git.bemagri.xyz/bernardo/nomarchy.git";
|
||||
nomarchyInstall = pkgs.callPackage ./pkgs/nomarchy-install {
|
||||
templateDir = ./templates/downstream;
|
||||
nomarchyLock = ./flake.lock;
|
||||
hardwareModuleNames = pkgs.writeText "nixos-hardware-modules.txt"
|
||||
(nixpkgs.lib.concatStringsSep "\n"
|
||||
(builtins.attrNames nixos-hardware.nixosModules));
|
||||
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
|
||||
{
|
||||
# ─── Downstream API ────────────────────────────────────────────
|
||||
@@ -52,6 +85,14 @@
|
||||
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 = {
|
||||
path = ./templates/downstream;
|
||||
description = "Nomarchy machine configuration (downstream flake)";
|
||||
@@ -59,10 +100,35 @@
|
||||
|
||||
packages.${system} = {
|
||||
nomarchy-theme-sync = pkgs.nomarchy-theme-sync;
|
||||
nomarchy-install = nomarchyInstall;
|
||||
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 ────────────────────────────────────────────
|
||||
# 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:
|
||||
# system → sudo nixos-rebuild switch --flake .#nomarchy (rare)
|
||||
# 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
|
||||
# switching on the live system works without a network: the
|
||||
# lockfile's narHashes resolve against these paths instead
|
||||
# of fetching from GitHub.
|
||||
system.extraDependencies = [
|
||||
nixpkgs.outPath
|
||||
home-manager.outPath
|
||||
stylix.outPath
|
||||
];
|
||||
# of fetching from GitHub. Must be *transitive* — evaluating
|
||||
# the seeded flake also fetches stylix's own inputs.
|
||||
system.extraDependencies =
|
||||
let
|
||||
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).
|
||||
Theme switching: nomarchy-theme-sync apply <name> (or SUPER+T)
|
||||
Wallpapers: nomarchy-theme-sync bg next (or SUPER+SHIFT+T)
|
||||
Install to disk: nomarchy-install
|
||||
The flake lives at ~/.nomarchy.
|
||||
'';
|
||||
|
||||
@@ -76,7 +77,7 @@
|
||||
# Welcome toast once the session is up (concatenated onto the
|
||||
# base exec-once list).
|
||||
exec-once = [
|
||||
"sh -c 'sleep 3; notify-send -a Nomarchy \"Welcome to Nomarchy\" \"SUPER+Return terminal · SUPER+T themes · SUPER+SHIFT+T wallpaper\"'"
|
||||
"sh -c 'sleep 3; notify-send -a Nomarchy \"Welcome to Nomarchy\" \"SUPER+Return terminal · SUPER+T themes · install with nomarchy-install\"'"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
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.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)
|
||||
libnotify
|
||||
];
|
||||
|
||||
@@ -26,6 +26,11 @@ in
|
||||
package = 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 = {
|
||||
"$mod" = "SUPER";
|
||||
"$terminal" = config.nomarchy.terminal;
|
||||
@@ -33,9 +38,10 @@ in
|
||||
monitor = [ ",preferred,auto,1" ];
|
||||
|
||||
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
|
||||
# swww-daemon internally).
|
||||
# the daemon internally).
|
||||
"nomarchy-theme-sync wallpaper"
|
||||
];
|
||||
|
||||
@@ -89,13 +95,13 @@ in
|
||||
touchpad.natural_scroll = true;
|
||||
};
|
||||
|
||||
dwindle = {
|
||||
pseudotile = true;
|
||||
preserve_split = true;
|
||||
};
|
||||
# dwindle:pseudotile was removed in Hyprland 0.55 (the `pseudo`
|
||||
# dispatcher still exists); setting it aborts config parsing.
|
||||
dwindle.preserve_split = true;
|
||||
|
||||
misc = {
|
||||
disable_hyprland_logo = true;
|
||||
disable_splash_rendering = true;
|
||||
force_default_wallpaper = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -26,9 +26,16 @@ in
|
||||
|
||||
services.greetd = lib.mkIf cfg.greeter.enable {
|
||||
enable = lib.mkDefault true;
|
||||
settings.default_session = {
|
||||
command = lib.mkDefault "${pkgs.greetd.tuigreet}/bin/tuigreet --time --remember --cmd Hyprland";
|
||||
user = "greeter";
|
||||
settings = {
|
||||
default_session = {
|
||||
command = lib.mkDefault "${pkgs.tuigreet}/bin/tuigreet --time --remember --cmd Hyprland";
|
||||
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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -53,6 +60,24 @@ in
|
||||
hardware.bluetooth.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 = {
|
||||
packages = with pkgs; [
|
||||
@@ -82,7 +107,21 @@ in
|
||||
wl-clipboard
|
||||
grim
|
||||
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 = {
|
||||
|
||||
@@ -8,7 +8,26 @@
|
||||
{
|
||||
options.nomarchy.system = {
|
||||
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; };
|
||||
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
|
||||
, python3
|
||||
, makeWrapper
|
||||
, swww
|
||||
, awww
|
||||
, libnotify
|
||||
, git
|
||||
# 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 —
|
||||
# the rebuild must use the user's own home-manager from their PATH.
|
||||
wrapProgram $out/bin/nomarchy-theme-sync \
|
||||
--prefix PATH : ${lib.makeBinPath [ swww libnotify git ]} \
|
||||
--prefix PATH : ${lib.makeBinPath [ awww libnotify git ]} \
|
||||
${lib.optionalString (themesDir != null)
|
||||
"--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:
|
||||
log(f"no wallpaper for theme '{state.get('slug', '?')}', skipping")
|
||||
return
|
||||
if shutil.which("swww") is None:
|
||||
log("swww not found, skipping wallpaper")
|
||||
# nixpkgs renamed swww to awww (CLI-compatible fork); accept either.
|
||||
swww = shutil.which("awww") or shutil.which("swww")
|
||||
if swww is None:
|
||||
log("awww/swww not found, skipping wallpaper")
|
||||
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):
|
||||
if subprocess.run(["swww", "query"], capture_output=True).returncode == 0:
|
||||
if subprocess.run([swww, "query"], capture_output=True).returncode == 0:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
result = subprocess.run(
|
||||
["swww", "img", str(wallpaper),
|
||||
[swww, "img", str(wallpaper),
|
||||
"--transition-type", "grow",
|
||||
"--transition-pos", "center",
|
||||
"--transition-duration", "1",
|
||||
@@ -185,7 +187,7 @@ def apply_wallpaper(state: dict, wait: bool = False) -> None:
|
||||
capture_output=True,
|
||||
)
|
||||
log(f"wallpaper: {wallpaper.name}" if result.returncode == 0
|
||||
else "wallpaper: swww failed (daemon not running?)")
|
||||
else "wallpaper: awww failed (daemon not running?)")
|
||||
|
||||
|
||||
# ─── Commands ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
# My Nomarchy machine
|
||||
|
||||
1. `nixos-generate-config --show-hardware-config > hardware-configuration.nix`
|
||||
2. Edit `flake.nix` (Nomarchy repo URL, username), `system.nix` (hostname,
|
||||
user), `home.nix` (your packages).
|
||||
1. Overwrite the placeholder hardware config with the real one:
|
||||
`nixos-generate-config --show-hardware-config > hardware-configuration.nix`
|
||||
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
|
||||
`theme-state.json`.
|
||||
4. System: `sudo nixos-rebuild switch --flake .#default`
|
||||
|
||||
@@ -1,53 +1,20 @@
|
||||
{
|
||||
description = "My Nomarchy machine";
|
||||
|
||||
inputs = {
|
||||
# Pin Nomarchy; nixpkgs and home-manager follow whatever it ships.
|
||||
nomarchy.url = "github:YOUR-USER/nomarchy"; # <- point at the distro repo
|
||||
nixpkgs.follows = "nomarchy/nixpkgs";
|
||||
home-manager.follows = "nomarchy/home-manager";
|
||||
};
|
||||
# The only input. nixpkgs, home-manager etc. come pinned through it —
|
||||
# tested together upstream. This file is written once (by you or the
|
||||
# installer) and never hand-edited afterwards; your machine lives in
|
||||
# system.nix and home.nix.
|
||||
inputs.nomarchy.url = "github:YOUR-USER/nomarchy"; # <- the distro repo
|
||||
|
||||
outputs = { self, nomarchy, nixpkgs, home-manager, ... }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
outputs = { nomarchy, ... }:
|
||||
nomarchy.lib.mkFlake {
|
||||
src = ./.;
|
||||
username = "me"; # <- your login name
|
||||
|
||||
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;
|
||||
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}";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
# Optional: a nixos-hardware module name for your machine, e.g.
|
||||
# hardwareProfile = "framework-13-7040-amd";
|
||||
# Names: https://github.com/NixOS/nixos-hardware
|
||||
# (the future installer fills this in automatically from DMI data)
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
# any of its defaults here with plain NixOS options (they use mkDefault),
|
||||
# or via the nomarchy.system.* toggles.
|
||||
{ pkgs, ... }:
|
||||
{ pkgs, username, ... }:
|
||||
|
||||
{
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
@@ -12,7 +12,8 @@
|
||||
time.timeZone = "UTC";
|
||||
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;
|
||||
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