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