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