···4747 image.fileName = lib.mkForce "atlas.iso";
4848 };
49495050- # PXE/netboot: iPXE pulls bzImage + initrd, so nothing has to be flashed to
5151- # iterate. The whole store rides inside the initrd (~3.8 GB), which is why
5252- # it has to be served over HTTP rather than TFTP — see nat-share --pxe/--http.
5353- image.modules.netboot = {
5454- imports = [ (modulesPath + "/installer/netboot/netboot.nix") ];
5555- netboot.squashfsCompression = "zstd -Xcompression-level 6";
5656- };
5050+ # PXE/netboot: iPXE pulls only bzImage + a ~11 MB initrd, so nothing has to
5151+ # be flashed to iterate.
5252+ #
5353+ # The store is NOT embedded in the initrd the way upstream netboot.nix does
5454+ # it. iPXE has to hold whatever it downloads in EfiBootServicesData, and
5555+ # firmware caps that well below installed RAM — a 2.8 GB initrd died with
5656+ # "No space left on device" (ipxe.org/34182006) on a 16 GB machine. So stage
5757+ # 1 brings up the network itself and fetches the squashfs, which lands in
5858+ # kernel-managed memory where the whole 16 GB is actually usable.
5959+ #
6060+ # The file is fetched to exactly the path netboot.nix already mounts from
6161+ # ("../nix-store.squashfs", i.e. /nix-store.squashfs in the initrd root), so
6262+ # the mount itself is upstream's, unmodified. It survives switch_root
6363+ # because the loop device keeps the inode open after the old root is wiped.
6464+ image.modules.netboot =
6565+ { config, lib, ... }:
6666+ {
6767+ imports = [ (modulesPath + "/installer/netboot/netboot.nix") ];
6868+ netboot.squashfsCompression = "zstd -Xcompression-level 6";
6969+7070+ # Ship the initrd without the store; pxe-serve serves squashfsStore
7171+ # separately over HTTP.
7272+ system.build.netbootRamdisk = lib.mkForce config.system.build.initialRamdisk;
7373+7474+ # DHCP in stage 1 (runs in preLVMCommands, before postDeviceCommands).
7575+ boot.initrd.network.enable = true;
7676+ boot.initrd.network.udhcpc.enable = true;
7777+7878+ # hardware.enableAllHardware is storage-only — virtio_net is the single
7979+ # network driver in its 89 modules — so wired NICs have to be listed by
8080+ # hand. availableKernelModules only makes them available; udev loads just
8181+ # the ones matching present PCI/USB IDs, so unused entries cost bytes.
8282+ boot.initrd.availableKernelModules = [
8383+ "e1000e"
8484+ "e1000"
8585+ "igb"
8686+ "igc"
8787+ "ixgbe"
8888+ "r8169"
8989+ "alx"
9090+ "atl1c"
9191+ "tg3"
9292+ "bnx2"
9393+ "bnx2x"
9494+ "sky2"
9595+ "forcedeth"
9696+ "atlantic"
9797+ # USB NICs (docks, adapters)
9898+ "r8152"
9999+ "cdc_ether"
100100+ "cdc_ncm"
101101+ "asix"
102102+ "ax88179_178a"
103103+ "virtio_net"
104104+ ];
105105+106106+ # wget is busybox's, already present in the scripted initrd's
107107+ # extra-utils. The URL comes from the kernel command line rather than
108108+ # being baked in, so the image doesn't go stale when the serving address
109109+ # changes (pxe-serve passes store.url=).
110110+ boot.initrd.postDeviceCommands = ''
111111+ store_url=
112112+ for o in $(cat /proc/cmdline); do
113113+ case $o in
114114+ store.url=*) store_url=''${o#store.url=} ;;
115115+ esac
116116+ done
117117+ if [ -z "$store_url" ]; then
118118+ echo "netboot: no store.url= on the kernel command line"
119119+ fail
120120+ fi
121121+ echo "netboot: fetching nix store from $store_url"
122122+ if ! wget -O /nix-store.squashfs "$store_url"; then
123123+ echo "netboot: failed to fetch $store_url"
124124+ fail
125125+ fi
126126+ '';
127127+ };
5712858129 nixpkgs.hostPlatform = "x86_64-linux";
59130
···129129INITRD=$(nix build --no-link --print-out-paths "${CFG}.system.build.netbootRamdisk")
130130TOPLEVEL=$(nix eval --raw "${CFG}.system.build.toplevel")
131131PARAMS=$(nix eval --raw --apply 'ps: builtins.concatStringsSep " " ps' "${CFG}.boot.kernelParams")
132132+# Served separately from the initrd: iPXE can only hold so much in
133133+# EfiBootServicesData, so stage 1 fetches this itself over HTTP.
134134+SQUASHFS=$(nix build --no-link --print-out-paths "${CFG}.system.build.squashfsStore")
132135133136# iPXE gets the chain script baked in rather than asking DHCP for a filename.
134137# In proxy-DHCP mode dnsmasq answers through pxe-service, so a --dhcp-boot
···173176174177ln -s "${KERNEL}/${KERNEL_FILE}" "$ROOT/bzImage"
175178ln -s "${INITRD}/initrd" "$ROOT/initrd"
179179+ln -s "${SQUASHFS}" "$ROOT/nix-store.squashfs"
176180ln -s "${IPXE}/ipxe.efi" "$ROOT/ipxe.efi"
177181ln -s "${IPXE}/undionly.kpxe" "$ROOT/undionly.kpxe"
178182183183+# store.url= is what stage 1 fetches the squashfs from; passing it on the
184184+# command line instead of baking it into the image keeps the image valid when
185185+# the serving address changes.
179186cat >"$ROOT/netboot.ipxe" <<EOF
180187#!ipxe
181181-kernel http://${ADDR}:${HTTP_PORT}/bzImage init=${TOPLEVEL}/init initrd=initrd ${PARAMS}
188188+kernel http://${ADDR}:${HTTP_PORT}/bzImage init=${TOPLEVEL}/init initrd=initrd ${PARAMS} store.url=http://${ADDR}:${HTTP_PORT}/nix-store.squashfs
182189initrd http://${ADDR}:${HTTP_PORT}/initrd
183190boot
184191EOF
185192186186-echo "$PROG: serving ${MACHINE} on ${IFACE} (${ADDR}), kernel+initrd over http://${ADDR}:${HTTP_PORT}"
187187-echo "$PROG: initrd is $(stat -Lc %s "$ROOT/initrd" | awk '{printf "%.1f GB", $1/1073741824}') — expect that much to cross the wire on every boot"
193193+echo "$PROG: serving ${MACHINE} on ${IFACE} (${ADDR}) over http://${ADDR}:${HTTP_PORT}"
194194+echo "$PROG: iPXE fetches $(stat -Lc %s "$ROOT/bzImage" | awk '{printf "%.0f MB", $1/1048576}') kernel + $(stat -Lc %s "$ROOT/initrd" | awk '{printf "%.0f MB", $1/1048576}') initrd; stage 1 then fetches $(stat -Lc %s "$ROOT/nix-store.squashfs" | awk '{printf "%.2f GB", $1/1073741824}') of store itself"
188195189196# exec, so there is no wrapper process between the terminal and dnsmasq: Ctrl+C
190197# reaches it directly and pxe-share's own trap does the privileged teardown