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
|
# 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`
|
## 2026-08-20 – The checkout on a machine lives in `~/flakes/moonix`
|
||||||
|
|
||||||
- **Who**: Maintainer, ClaudeCode
|
- **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.
|
# Colloid overrides, used by the desktop session and by the greeter.
|
||||||
# Defined once so the two cannot drift apart.
|
# Defined once so the two cannot drift apart.
|
||||||
themePackages = {
|
themePackages = {
|
||||||
@@ -68,7 +80,10 @@
|
|||||||
};
|
};
|
||||||
in
|
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
|
# Makes the packages and the moonarch source tree available to every
|
||||||
# module without threading arguments through by hand.
|
# module without threading arguments through by hand.
|
||||||
@@ -90,6 +105,17 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
nixosConfigurations = {
|
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.
|
# QEMU/KVM test VM.
|
||||||
testvm = nixpkgs.lib.nixosSystem {
|
testvm = nixpkgs.lib.nixosSystem {
|
||||||
inherit system;
|
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