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