diff --git a/DECISIONS.md b/DECISIONS.md index 4e1d598..7e15722 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -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 `: 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 diff --git a/flake.nix b/flake.nix index cc3d114..4889bb4 100644 --- a/flake.nix +++ b/flake.nix @@ -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; diff --git a/hosts/installer/default.nix b/hosts/installer/default.nix new file mode 100644 index 0000000..c0736ff --- /dev/null +++ b/hosts/installer/default.nix @@ -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 # 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" ]; +}