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 = pkgs.callPackage ../../../lib/make-ext4-fs.nix ({
16 inherit (config.sdImage) storePaths;
17 volumeLabel = "NIXOS_SD";
18 } // optionalAttrs (config.sdImage.rootPartitionUUID != null) {
19 uuid = config.sdImage.rootPartitionUUID;
20 });
21in
22{
23 options.sdImage = {
24 imageName = mkOption {
25 default = "${config.sdImage.imageBaseName}-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.img";
26 description = ''
27 Name of the generated image file.
28 '';
29 };
30
31 imageBaseName = mkOption {
32 default = "nixos-sd-image";
33 description = ''
34 Prefix of the name of the generated image file.
35 '';
36 };
37
38 storePaths = mkOption {
39 type = with types; listOf package;
40 example = literalExample "[ pkgs.stdenv ]";
41 description = ''
42 Derivations to be included in the Nix store in the generated SD image.
43 '';
44 };
45
46 bootPartitionID = mkOption {
47 type = types.string;
48 default = "0x2178694e";
49 description = ''
50 Volume ID for the /boot partition on the SD card. This value must be a
51 32-bit hexadecimal number.
52 '';
53 };
54
55 rootPartitionUUID = mkOption {
56 type = types.nullOr types.string;
57 default = null;
58 example = "14e19a7b-0ae0-484d-9d54-43bd6fdc20c7";
59 description = ''
60 UUID for the main NixOS partition on the SD card.
61 '';
62 };
63
64 bootSize = mkOption {
65 type = types.int;
66 default = 120;
67 description = ''
68 Size of the /boot partition, in megabytes.
69 '';
70 };
71
72 populateBootCommands = mkOption {
73 example = literalExample "'' cp \${pkgs.myBootLoader}/u-boot.bin boot/ ''";
74 description = ''
75 Shell commands to populate the ./boot directory.
76 All files in that directory are copied to the
77 /boot partition on the SD image.
78 '';
79 };
80 };
81
82 config = {
83 fileSystems = {
84 "/boot" = {
85 device = "/dev/disk/by-label/NIXOS_BOOT";
86 fsType = "vfat";
87 };
88 "/" = {
89 device = "/dev/disk/by-label/NIXOS_SD";
90 fsType = "ext4";
91 };
92 };
93
94 sdImage.storePaths = [ config.system.build.toplevel ];
95
96 system.build.sdImage = pkgs.callPackage ({ stdenv, dosfstools, e2fsprogs, mtools, libfaketime, utillinux }: stdenv.mkDerivation {
97 name = config.sdImage.imageName;
98
99 nativeBuildInputs = [ dosfstools e2fsprogs mtools libfaketime utillinux ];
100
101 buildCommand = ''
102 mkdir -p $out/nix-support $out/sd-image
103 export img=$out/sd-image/${config.sdImage.imageName}
104
105 echo "${pkgs.stdenv.buildPlatform.system}" > $out/nix-support/system
106 echo "file sd-image $img" >> $out/nix-support/hydra-build-products
107
108 # Create the image file sized to fit /boot and /, plus 20M of slack
109 rootSizeBlocks=$(du -B 512 --apparent-size ${rootfsImage} | awk '{ print $1 }')
110 bootSizeBlocks=$((${toString config.sdImage.bootSize} * 1024 * 1024 / 512))
111 imageSize=$((rootSizeBlocks * 512 + bootSizeBlocks * 512 + 20 * 1024 * 1024))
112 truncate -s $imageSize $img
113
114 # type=b is 'W95 FAT32', type=83 is 'Linux'.
115 sfdisk $img <<EOF
116 label: dos
117 label-id: ${config.sdImage.bootPartitionID}
118
119 start=8M, size=$bootSizeBlocks, type=b, bootable
120 start=${toString (8 + config.sdImage.bootSize)}M, type=83
121 EOF
122
123 # Copy the rootfs into the SD image
124 eval $(partx $img -o START,SECTORS --nr 2 --pairs)
125 dd conv=notrunc if=${rootfsImage} of=$img seek=$START count=$SECTORS
126
127 # Create a FAT32 /boot partition of suitable size into bootpart.img
128 eval $(partx $img -o START,SECTORS --nr 1 --pairs)
129 truncate -s $((SECTORS * 512)) bootpart.img
130 faketime "1970-01-01 00:00:00" mkfs.vfat -i ${config.sdImage.bootPartitionID} -n NIXOS_BOOT bootpart.img
131
132 # Populate the files intended for /boot
133 mkdir boot
134 ${config.sdImage.populateBootCommands}
135
136 # Copy the populated /boot into the SD image
137 (cd boot; mcopy -bpsvm -i ../bootpart.img ./* ::)
138 dd conv=notrunc if=bootpart.img of=$img seek=$START count=$SECTORS
139 '';
140 }) {};
141
142 boot.postBootCommands = ''
143 # On the first boot do some maintenance tasks
144 if [ -f /nix-path-registration ]; then
145 # Figure out device names for the boot device and root filesystem.
146 rootPart=$(readlink -f /dev/disk/by-label/NIXOS_SD)
147 bootDevice=$(lsblk -npo PKNAME $rootPart)
148
149 # Resize the root partition and the filesystem to fit the disk
150 echo ",+," | sfdisk -N2 --no-reread $bootDevice
151 ${pkgs.parted}/bin/partprobe
152 ${pkgs.e2fsprogs}/bin/resize2fs $rootPart
153
154 # Register the contents of the initial Nix store
155 ${config.nix.package.out}/bin/nix-store --load-db < /nix-path-registration
156
157 # nixos-rebuild also requires a "system" profile and an /etc/NIXOS tag.
158 touch /etc/NIXOS
159 ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
160
161 # Prevents this from running on later boots.
162 rm -f /nix-path-registration
163 fi
164 '';
165 };
166}