feat(installer): ISO that installs a host with one command
hosts/installer builds an image (nix build .#installer-iso) carrying this flake as /etc/moonix, disko and a wrapper. The guest runs `moonix-install <host>`: disko formats per the host file, nixos-install installs, and the repository is cloned into the account own ~/flakes/moonix with /etc/nixos/flake.nix pointing at it. The image is built from the local tree, so a state does not have to be pushed to be installable. disko is patched to re-ask for the LUKS passphrase on a mismatch instead of killing the script: its `exit 1` sits inside a function called from the until loop that was meant to repeat the prompt.
This commit is contained in:
@@ -1,5 +1,30 @@
|
||||
# Decisions
|
||||
|
||||
## 2026-08-20 – Installing from an own ISO, one command
|
||||
|
||||
- **Who**: Maintainer, ClaudeCode
|
||||
- **Why**: The documented install was a five-line `nix run` invocation typed at
|
||||
the guest console, reading the configuration from Gitea — so only pushed
|
||||
states were installable, and a typo in a long command cost the whole run.
|
||||
- **Tradeoffs**: `nixos-anywhere` and an SSH-driven install were ruled out: the
|
||||
machine is installed in front of it, not over the network. A script or flake
|
||||
app in the repository still has to be fetched before it can run, which is the
|
||||
problem it was meant to solve. Baking the target closure into the image would
|
||||
make the install work offline, at four to six gigabytes per rebuild — not
|
||||
taken.
|
||||
- **How**: `hosts/installer` builds an ISO (`nix build .#installer-iso`) that
|
||||
carries this flake as `/etc/moonix`, disko and one wrapper. The guest runs
|
||||
`moonix-install <host>`: partition and format per the host file, install, then
|
||||
clone the repository into the account's `~/flakes/moonix`, hand it to the
|
||||
account and point `/etc/nixos/flake.nix` at it. The image is built from the
|
||||
local tree, so unpushed changes are installable. Network is still needed —
|
||||
the packages come from the binary cache.
|
||||
- **Also**: disko aborts the whole format script when the two passphrase
|
||||
entries differ, because `exit 1` sits inside a function called from the
|
||||
`until` loop meant to repeat the prompt
|
||||
(`share/disko/lib/types/luks.nix:151`). The installer uses a disko patched to
|
||||
`return 1` there, so a typo asks again instead of restarting the install.
|
||||
|
||||
## 2026-08-20 – The checkout on a machine lives in `~/flakes/moonix`
|
||||
|
||||
- **Who**: Maintainer, ClaudeCode
|
||||
|
||||
@@ -52,6 +52,18 @@
|
||||
};
|
||||
};
|
||||
|
||||
# disko kills the whole format script when the two passphrase entries
|
||||
# differ: `exit 1` sits inside a function called from the `until` loop
|
||||
# that was meant to repeat the prompt
|
||||
# (share/disko/lib/types/luks.nix:151). A typo therefore means starting
|
||||
# the install over. One word changes that.
|
||||
diskoRetryingPassphrase = inputs.disko.packages.${system}.disko.overrideAttrs (old: {
|
||||
postFixup = (old.postFixup or "") + ''
|
||||
substituteInPlace $out/share/disko/lib/types/luks.nix \
|
||||
--replace-fail 'exit 1' 'return 1'
|
||||
'';
|
||||
});
|
||||
|
||||
# Colloid overrides, used by the desktop session and by the greeter.
|
||||
# Defined once so the two cannot drift apart.
|
||||
themePackages = {
|
||||
@@ -68,7 +80,10 @@
|
||||
};
|
||||
in
|
||||
{
|
||||
packages.${system} = moonarchPackages;
|
||||
packages.${system} = moonarchPackages // {
|
||||
# Installer image: `nix build .#installer-iso`.
|
||||
installer-iso = self.nixosConfigurations.installer.config.system.build.isoImage;
|
||||
};
|
||||
|
||||
# Makes the packages and the moonarch source tree available to every
|
||||
# module without threading arguments through by hand.
|
||||
@@ -90,6 +105,17 @@
|
||||
};
|
||||
|
||||
nixosConfigurations = {
|
||||
# Installer image. Carries this flake and disko, so installing needs
|
||||
# neither a clone nor the Gitea reference.
|
||||
installer = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
modules = [ ./hosts/installer ];
|
||||
specialArgs = {
|
||||
moonixSrc = self;
|
||||
disko = diskoRetryingPassphrase;
|
||||
};
|
||||
};
|
||||
|
||||
# QEMU/KVM test VM.
|
||||
testvm = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# ABOUTME: Installer ISO carrying the moonix flake, disko and one install command.
|
||||
# ABOUTME: Built with `nix build .#installer-iso`; installs a host from the image.
|
||||
|
||||
{ pkgs, modulesPath, moonixSrc, disko, ... }:
|
||||
|
||||
let
|
||||
# Wraps the flags that never change, so the guest console sees one short
|
||||
# command instead of a line that has to be typed without a typo.
|
||||
moonix-install = pkgs.writeShellApplication {
|
||||
name = "moonix-install";
|
||||
runtimeInputs = [ disko pkgs.nixos-install-tools pkgs.git pkgs.nix pkgs.gawk ];
|
||||
text = ''
|
||||
host=''${1:-}
|
||||
if [[ -z "$host" ]]; then
|
||||
echo "usage: moonix-install <host> # testvm, thinkpad, desktop" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Two steps instead of disko-install, which demands a --disk flag per
|
||||
# disk and overrides the declared device anyway
|
||||
# (disko/install-cli.nix:14). This way the devices stay declared in the
|
||||
# host file and nothing has to be typed twice.
|
||||
#
|
||||
# Partition, format and mount at /mnt. Erases the disks the host file
|
||||
# names.
|
||||
disko --mode destroy,format,mount --flake /etc/moonix#"$host"
|
||||
|
||||
# Install onto what disko just mounted. The bootloader writes its NVRAM
|
||||
# entry because the host files set boot.loader.efi.canTouchEfiVariables.
|
||||
nixos-install --flake /etc/moonix#"$host" --no-root-password
|
||||
|
||||
# The image carries the flake as a store copy, which cannot be committed
|
||||
# from. Put a real checkout in the account's home instead, owned by it,
|
||||
# and point /etc/nixos at it — that is what nixos-rebuild reads later.
|
||||
user=$(nix eval --raw "/etc/moonix#nixosConfigurations.$host.config.moonarch.user")
|
||||
owner=$(awk -F: -v u="$user" '$1 == u { print $3 ":" $4 }' /mnt/etc/passwd)
|
||||
|
||||
git clone https://gitea.moonarch.de/nevaforget/moonix.git \
|
||||
"/mnt/home/$user/flakes/moonix"
|
||||
git -C "/mnt/home/$user/flakes/moonix" remote set-url origin \
|
||||
git@gitea.moonarch.de:nevaforget/moonix.git
|
||||
chown -R "$owner" "/mnt/home/$user/flakes"
|
||||
|
||||
mkdir -p /mnt/etc/nixos
|
||||
ln -sfn "/home/$user/flakes/moonix/flake.nix" /mnt/etc/nixos/flake.nix
|
||||
|
||||
echo "Installed $host. Checkout: /home/$user/flakes/moonix" >&2
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [ (modulesPath + "/installer/cd-dvd/installation-cd-minimal.nix") ];
|
||||
|
||||
# The flake travels inside the image, so the install reads the revision this
|
||||
# ISO was built from — including changes that are not pushed yet.
|
||||
environment.etc.moonix.source = moonixSrc;
|
||||
|
||||
environment.systemPackages = [ moonix-install ];
|
||||
|
||||
# Same layout the installed systems use. The LUKS passphrase is typed twice
|
||||
# per container here, so the layout has to match what it will be at boot.
|
||||
console.keyMap = "de";
|
||||
|
||||
# disko-install evaluates the target configuration, which needs flakes.
|
||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
||||
}
|
||||
Reference in New Issue
Block a user