1# This module creates a bootable ISO image containing the given NixOS
2# configuration. The derivation for the ISO image will be placed in
3# config.system.build.isoImage.
4
5{ config, lib, pkgs, ... }:
6
7with lib;
8
9let
10 # Timeout in syslinux is in units of 1/10 of a second.
11 # 0 is used to disable timeouts.
12 syslinuxTimeout = if config.boot.loader.timeout == null then
13 0
14 else
15 max (config.boot.loader.timeout * 10) 1;
16
17
18 max = x: y: if x > y then x else y;
19
20 # The configuration file for syslinux.
21
22 # Notes on syslinux configuration and UNetbootin compatiblity:
23 # * Do not use '/syslinux/syslinux.cfg' as the path for this
24 # configuration. UNetbootin will not parse the file and use it as-is.
25 # This results in a broken configuration if the partition label does
26 # not match the specified config.isoImage.volumeID. For this reason
27 # we're using '/isolinux/isolinux.cfg'.
28 # * Use APPEND instead of adding command-line arguments directly after
29 # the LINUX entries.
30 # * COM32 entries (chainload, reboot, poweroff) are not recognized. They
31 # result in incorrect boot entries.
32
33 baseIsolinuxCfg = ''
34 SERIAL 0 38400
35 TIMEOUT ${builtins.toString syslinuxTimeout}
36 UI vesamenu.c32
37 MENU TITLE NixOS
38 MENU BACKGROUND /isolinux/background.png
39 DEFAULT boot
40
41 LABEL boot
42 MENU LABEL NixOS ${config.system.nixosLabel}${config.isoImage.appendToMenuLabel}
43 LINUX /boot/bzImage
44 APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}
45 INITRD /boot/initrd
46
47 # A variant to boot with 'nomodeset'
48 LABEL boot-nomodeset
49 MENU LABEL NixOS ${config.system.nixosVersion}${config.isoImage.appendToMenuLabel} (with nomodeset)
50 LINUX /boot/bzImage
51 APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} nomodeset
52 INITRD /boot/initrd
53
54 # A variant to boot with 'copytoram'
55 LABEL boot-copytoram
56 MENU LABEL NixOS ${config.system.nixosVersion}${config.isoImage.appendToMenuLabel} (with copytoram)
57 LINUX /boot/bzImage
58 APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} copytoram
59 INITRD /boot/initrd
60 '';
61
62 isolinuxMemtest86Entry = ''
63 LABEL memtest
64 MENU LABEL Memtest86+
65 LINUX /boot/memtest.bin
66 APPEND ${toString config.boot.loader.grub.memtest86.params}
67 '';
68
69 isolinuxCfg = baseIsolinuxCfg + (optionalString config.boot.loader.grub.memtest86.enable isolinuxMemtest86Entry);
70
71 # The EFI boot image.
72 efiDir = pkgs.runCommand "efi-directory" {} ''
73 mkdir -p $out/EFI/boot
74 cp -v ${pkgs.systemd}/lib/systemd/boot/efi/systemd-boot${targetArch}.efi $out/EFI/boot/boot${targetArch}.efi
75 mkdir -p $out/loader/entries
76
77 echo "title NixOS Live CD" > $out/loader/entries/nixos-livecd.conf
78 echo "linux /boot/bzImage" >> $out/loader/entries/nixos-livecd.conf
79 echo "initrd /boot/initrd" >> $out/loader/entries/nixos-livecd.conf
80 echo "options init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}" >> $out/loader/entries/nixos-livecd.conf
81
82 # A variant to boot with 'nomodeset'
83 echo "title NixOS Live CD (with nomodeset)" > $out/loader/entries/nixos-livecd-nomodeset.conf
84 echo "linux /boot/bzImage" >> $out/loader/entries/nixos-livecd-nomodeset.conf
85 echo "initrd /boot/initrd" >> $out/loader/entries/nixos-livecd-nomodeset.conf
86 echo "options init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} nomodeset" >> $out/loader/entries/nixos-livecd-nomodeset.conf
87
88 # A variant to boot with 'copytoram'
89 echo "title NixOS Live CD (with copytoram)" > $out/loader/entries/nixos-livecd-copytoram.conf
90 echo "linux /boot/bzImage" >> $out/loader/entries/nixos-livecd-copytoram.conf
91 echo "initrd /boot/initrd" >> $out/loader/entries/nixos-livecd-copytoram.conf
92 echo "options init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} copytoram" >> $out/loader/entries/nixos-livecd-copytoram.conf
93
94 echo "default nixos-livecd" > $out/loader/loader.conf
95 echo "timeout ${builtins.toString config.boot.loader.timeout}" >> $out/loader/loader.conf
96 '';
97
98 efiImg = pkgs.runCommand "efi-image_eltorito" { buildInputs = [ pkgs.mtools pkgs.libfaketime ]; }
99 # Be careful about determinism: du --apparent-size,
100 # dates (cp -p, touch, mcopy -m, faketime for label), IDs (mkfs.vfat -i)
101 ''
102 mkdir ./contents && cd ./contents
103 cp -rp "${efiDir}"/* .
104 mkdir ./boot
105 cp -p "${config.boot.kernelPackages.kernel}/bzImage" \
106 "${config.system.build.initialRamdisk}/initrd" ./boot/
107 touch --date=@0 ./*
108
109 usage_size=$(du -sb --apparent-size . | tr -cd '[:digit:]')
110 # Make the image 110% as big as the files need to make up for FAT overhead
111 image_size=$(( ($usage_size * 110) / 100 ))
112 # Make the image fit blocks of 1M
113 block_size=$((1024*1024))
114 image_size=$(( ($image_size / $block_size + 1) * $block_size ))
115 echo "Usage size: $usage_size"
116 echo "Image size: $image_size"
117 truncate --size=$image_size "$out"
118 ${pkgs.libfaketime}/bin/faketime "2000-01-01 00:00:00" ${pkgs.dosfstools}/sbin/mkfs.vfat -i 12345678 -n EFIBOOT "$out"
119 mcopy -bpsvm -i "$out" ./* ::
120 ''; # */
121
122 targetArch = if pkgs.stdenv.isi686 then
123 "ia32"
124 else if pkgs.stdenv.isx86_64 then
125 "x64"
126 else
127 throw "Unsupported architecture";
128
129in
130
131{
132 options = {
133
134 isoImage.isoName = mkOption {
135 default = "${config.isoImage.isoBaseName}.iso";
136 description = ''
137 Name of the generated ISO image file.
138 '';
139 };
140
141 isoImage.isoBaseName = mkOption {
142 default = "nixos";
143 description = ''
144 Prefix of the name of the generated ISO image file.
145 '';
146 };
147
148 isoImage.compressImage = mkOption {
149 default = false;
150 description = ''
151 Whether the ISO image should be compressed using
152 <command>bzip2</command>.
153 '';
154 };
155
156 isoImage.volumeID = mkOption {
157 default = "NIXOS_BOOT_CD";
158 description = ''
159 Specifies the label or volume ID of the generated ISO image.
160 Note that the label is used by stage 1 of the boot process to
161 mount the CD, so it should be reasonably distinctive.
162 '';
163 };
164
165 isoImage.contents = mkOption {
166 example = literalExample ''
167 [ { source = pkgs.memtest86 + "/memtest.bin";
168 target = "boot/memtest.bin";
169 }
170 ]
171 '';
172 description = ''
173 This option lists files to be copied to fixed locations in the
174 generated ISO image.
175 '';
176 };
177
178 isoImage.storeContents = mkOption {
179 example = literalExample "[ pkgs.stdenv ]";
180 description = ''
181 This option lists additional derivations to be included in the
182 Nix store in the generated ISO image.
183 '';
184 };
185
186 isoImage.includeSystemBuildDependencies = mkOption {
187 default = false;
188 description = ''
189 Set this option to include all the needed sources etc in the
190 image. It significantly increases image size. Use that when
191 you want to be able to keep all the sources needed to build your
192 system or when you are going to install the system on a computer
193 with slow or non-existent network connection.
194 '';
195 };
196
197 isoImage.makeEfiBootable = mkOption {
198 default = false;
199 description = ''
200 Whether the ISO image should be an efi-bootable volume.
201 '';
202 };
203
204 isoImage.makeUsbBootable = mkOption {
205 default = false;
206 description = ''
207 Whether the ISO image should be bootable from CD as well as USB.
208 '';
209 };
210
211 isoImage.splashImage = mkOption {
212 default = pkgs.fetchurl {
213 url = https://raw.githubusercontent.com/NixOS/nixos-artwork/5729ab16c6a5793c10a2913b5a1b3f59b91c36ee/ideas/grub-splash/grub-nixos-1.png;
214 sha256 = "43fd8ad5decf6c23c87e9026170a13588c2eba249d9013cb9f888da5e2002217";
215 };
216 description = ''
217 The splash image to use in the bootloader.
218 '';
219 };
220
221 isoImage.appendToMenuLabel = mkOption {
222 default = " Installer";
223 example = " Live System";
224 description = ''
225 The string to append after the menu label for the NixOS system.
226 This will be directly appended (without whitespace) to the NixOS version
227 string, like for example if it is set to <literal>XXX</literal>:
228
229 <para><literal>NixOS 99.99-pre666XXX</literal></para>
230 '';
231 };
232
233 };
234
235 config = {
236
237 boot.loader.grub.version = 2;
238
239 # Don't build the GRUB menu builder script, since we don't need it
240 # here and it causes a cyclic dependency.
241 boot.loader.grub.enable = false;
242
243 # !!! Hack - attributes expected by other modules.
244 system.boot.loader.kernelFile = "bzImage";
245 environment.systemPackages = [ pkgs.grub2 pkgs.grub2_efi pkgs.syslinux ];
246
247 # In stage 1 of the boot, mount the CD as the root FS by label so
248 # that we don't need to know its device. We pass the label of the
249 # root filesystem on the kernel command line, rather than in
250 # `fileSystems' below. This allows CD-to-USB converters such as
251 # UNetbootin to rewrite the kernel command line to pass the label or
252 # UUID of the USB stick. It would be nicer to write
253 # `root=/dev/disk/by-label/...' here, but UNetbootin doesn't
254 # recognise that.
255 boot.kernelParams =
256 [ "root=LABEL=${config.isoImage.volumeID}"
257 "boot.shell_on_fail"
258 ];
259
260 fileSystems."/" =
261 { fsType = "tmpfs";
262 options = [ "mode=0755" ];
263 };
264
265 # Note that /dev/root is a symlink to the actual root device
266 # specified on the kernel command line, created in the stage 1
267 # init script.
268 fileSystems."/iso" =
269 { device = "/dev/root";
270 neededForBoot = true;
271 noCheck = true;
272 };
273
274 # In stage 1, mount a tmpfs on top of /nix/store (the squashfs
275 # image) to make this a live CD.
276 fileSystems."/nix/.ro-store" =
277 { fsType = "squashfs";
278 device = "/iso/nix-store.squashfs";
279 options = [ "loop" ];
280 neededForBoot = true;
281 };
282
283 fileSystems."/nix/.rw-store" =
284 { fsType = "tmpfs";
285 options = [ "mode=0755" ];
286 neededForBoot = true;
287 };
288
289 fileSystems."/nix/store" =
290 { fsType = "unionfs-fuse";
291 device = "unionfs";
292 options = [ "allow_other" "cow" "nonempty" "chroot=/mnt-root" "max_files=32768" "hide_meta_files" "dirs=/nix/.rw-store=rw:/nix/.ro-store=ro" ];
293 };
294
295 boot.initrd.availableKernelModules = [ "squashfs" "iso9660" "usb-storage" "uas" ];
296
297 boot.blacklistedKernelModules = [ "nouveau" ];
298
299 boot.initrd.kernelModules = [ "loop" ];
300
301 # Closures to be copied to the Nix store on the CD, namely the init
302 # script and the top-level system configuration directory.
303 isoImage.storeContents =
304 [ config.system.build.toplevel ] ++
305 optional config.isoImage.includeSystemBuildDependencies
306 config.system.build.toplevel.drvPath;
307
308 # Create the squashfs image that contains the Nix store.
309 system.build.squashfsStore = import ../../../lib/make-squashfs.nix {
310 inherit (pkgs) stdenv squashfsTools perl pathsFromGraph;
311 storeContents = config.isoImage.storeContents;
312 };
313
314 # Individual files to be included on the CD, outside of the Nix
315 # store on the CD.
316 isoImage.contents =
317 [ { source = pkgs.substituteAll {
318 name = "isolinux.cfg";
319 src = pkgs.writeText "isolinux.cfg-in" isolinuxCfg;
320 bootRoot = "/boot";
321 };
322 target = "/isolinux/isolinux.cfg";
323 }
324 { source = config.boot.kernelPackages.kernel + "/bzImage";
325 target = "/boot/bzImage";
326 }
327 { source = config.system.build.initialRamdisk + "/initrd";
328 target = "/boot/initrd";
329 }
330 { source = config.system.build.squashfsStore;
331 target = "/nix-store.squashfs";
332 }
333 { source = "${pkgs.syslinux}/share/syslinux";
334 target = "/isolinux";
335 }
336 { source = config.isoImage.splashImage;
337 target = "/isolinux/background.png";
338 }
339 ] ++ optionals config.isoImage.makeEfiBootable [
340 { source = efiImg;
341 target = "/boot/efi.img";
342 }
343 { source = "${efiDir}/EFI";
344 target = "/EFI";
345 }
346 { source = "${efiDir}/loader";
347 target = "/loader";
348 }
349 ] ++ optionals config.boot.loader.grub.memtest86.enable [
350 { source = "${pkgs.memtest86plus}/memtest.bin";
351 target = "/boot/memtest.bin";
352 }
353 ];
354
355 boot.loader.timeout = 10;
356
357 # Create the ISO image.
358 system.build.isoImage = import ../../../lib/make-iso9660-image.nix ({
359 inherit (pkgs) stdenv perl pathsFromGraph xorriso syslinux;
360
361 inherit (config.isoImage) isoName compressImage volumeID contents;
362
363 bootable = true;
364 bootImage = "/isolinux/isolinux.bin";
365 } // optionalAttrs config.isoImage.makeUsbBootable {
366 usbBootable = true;
367 isohybridMbrImage = "${pkgs.syslinux}/share/syslinux/isohdpfx.bin";
368 } // optionalAttrs config.isoImage.makeEfiBootable {
369 efiBootable = true;
370 efiBootImage = "boot/efi.img";
371 });
372
373 boot.postBootCommands =
374 ''
375 # After booting, register the contents of the Nix store on the
376 # CD in the Nix database in the tmpfs.
377 ${config.nix.package.out}/bin/nix-store --load-db < /nix/store/nix-path-registration
378
379 # nixos-rebuild also requires a "system" profile and an
380 # /etc/NIXOS tag.
381 touch /etc/NIXOS
382 ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
383 '';
384
385 # Add vfat support to the initrd to enable people to copy the
386 # contents of the CD to a bootable USB stick.
387 boot.initrd.supportedFilesystems = [ "vfat" ];
388
389 };
390
391}