1# This module creates a bootable SD card image containing the given NixOS
2# configuration. The generated image is MBR partitioned, with a FAT /boot
3# partition, and ext4 root partition. The generated image is sized to fit
4# its contents, and a boot script automatically resizes the root partition
5# to fit the device on the first boot.
6#
7# The derivation for the SD image will be placed in
8# config.system.build.sdImage
9
10{ config, lib, pkgs, ... }:
11
12with lib;
13
14let
15 rootfsImage = import ../../../lib/make-ext4-fs.nix {
16 inherit pkgs;
17 inherit (config.sdImage) storePaths;
18 volumeLabel = "NIXOS_SD";
19 };
20in
21{
22 options.sdImage = {
23 storePaths = mkOption {
24 type = with types; listOf package;
25 example = literalExample "[ pkgs.stdenv ]";
26 description = ''
27 Derivations to be included in the Nix store in the generated SD image.
28 '';
29 };
30
31 bootSize = mkOption {
32 type = types.int;
33 default = 120;
34 description = ''
35 Size of the /boot partition, in megabytes.
36 '';
37 };
38
39 populateBootCommands = mkOption {
40 example = literalExample "'' cp \${pkgs.myBootLoader}/u-boot.bin boot/ ''";
41 description = ''
42 Shell commands to populate the ./boot directory.
43 All files in that directory are copied to the
44 /boot partition on the SD image.
45 '';
46 };
47 };
48
49 config = {
50 fileSystems = {
51 "/boot" = {
52 device = "/dev/disk/by-label/NIXOS_BOOT";
53 fsType = "vfat";
54 };
55 "/" = {
56 device = "/dev/disk/by-label/NIXOS_SD";
57 fsType = "ext4";
58 };
59 };
60
61 sdImage.storePaths = [ config.system.build.toplevel ];
62
63 system.build.sdImage = pkgs.stdenv.mkDerivation {
64 name = "sd-image-${pkgs.stdenv.system}.img";
65
66 buildInputs = with pkgs; [ dosfstools e2fsprogs mtools libfaketime utillinux ];
67
68 buildCommand = ''
69 # Create the image file sized to fit /boot and /, plus 20M of slack
70 rootSizeBlocks=$(du -B 512 --apparent-size ${rootfsImage} | awk '{ print $1 }')
71 bootSizeBlocks=$((${toString config.sdImage.bootSize} * 1024 * 1024 / 512))
72 imageSize=$((rootSizeBlocks * 512 + bootSizeBlocks * 512 + 20 * 1024 * 1024))
73 truncate -s $imageSize $out
74
75 # type=b is 'W95 FAT32', type=83 is 'Linux'.
76 sfdisk $out <<EOF
77 label: dos
78 label-id: 0x2178694e
79
80 start=8M, size=$bootSizeBlocks, type=b, bootable
81 start=${toString (8 + config.sdImage.bootSize)}M, type=83
82 EOF
83
84 # Copy the rootfs into the SD image
85 eval $(partx $out -o START,SECTORS --nr 2 --pairs)
86 dd conv=notrunc if=${rootfsImage} of=$out seek=$START count=$SECTORS
87
88 # Create a FAT32 /boot partition of suitable size into bootpart.img
89 eval $(partx $out -o START,SECTORS --nr 1 --pairs)
90 truncate -s $((SECTORS * 512)) bootpart.img
91 faketime "1970-01-01 00:00:00" mkfs.vfat -i 0x2178694e -n NIXOS_BOOT bootpart.img
92
93 # Populate the files intended for /boot
94 mkdir boot
95 ${config.sdImage.populateBootCommands}
96
97 # Copy the populated /boot into the SD image
98 (cd boot; mcopy -bpsvm -i ../bootpart.img ./* ::)
99 dd conv=notrunc if=bootpart.img of=$out seek=$START count=$SECTORS
100 '';
101 };
102
103 boot.postBootCommands = ''
104 # On the first boot do some maintenance tasks
105 if [ -f /nix-path-registration ]; then
106 # Figure out device names for the boot device and root filesystem.
107 rootPart=$(readlink -f /dev/disk/by-label/NIXOS_SD)
108 bootDevice=$(lsblk -npo PKNAME $rootPart)
109
110 # Resize the root partition and the filesystem to fit the disk
111 echo ",+," | sfdisk -N2 --no-reread $bootDevice
112 ${pkgs.parted}/bin/partprobe
113 ${pkgs.e2fsprogs}/bin/resize2fs $rootPart
114
115 # Register the contents of the initial Nix store
116 ${config.nix.package.out}/bin/nix-store --load-db < /nix-path-registration
117
118 # nixos-rebuild also requires a "system" profile and an /etc/NIXOS tag.
119 touch /etc/NIXOS
120 ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
121
122 # Prevents this from running on later boots.
123 rm -f /nix-path-registration
124 fi
125 '';
126 };
127}