1# This module creates a bootable SD card image containing the given NixOS
2# configuration. The generated image is MBR partitioned, with a FAT
3# /boot/firmware partition, and ext4 root partition. The generated image
4# is sized to fit its contents, and a boot script automatically resizes
5# the root partition to fit the device on the first boot.
6#
7# The firmware partition is built with expectation to hold the Raspberry
8# Pi firmware and bootloader, and be removed and replaced with a firmware
9# build for the target SoC for other board families.
10#
11# The derivation for the SD image will be placed in
12# config.system.build.sdImage
13
14{ config, lib, pkgs, ... }:
15
16with lib;
17
18let
19 rootfsImage = pkgs.callPackage ../../../lib/make-ext4-fs.nix ({
20 inherit (config.sdImage) storePaths;
21 compressImage = true;
22 populateImageCommands = config.sdImage.populateRootCommands;
23 volumeLabel = "NIXOS_SD";
24 } // optionalAttrs (config.sdImage.rootPartitionUUID != null) {
25 uuid = config.sdImage.rootPartitionUUID;
26 });
27in
28{
29 imports = [
30 (mkRemovedOptionModule [ "sdImage" "bootPartitionID" ] "The FAT partition for SD image now only holds the Raspberry Pi firmware files. Use firmwarePartitionID to configure that partition's ID.")
31 (mkRemovedOptionModule [ "sdImage" "bootSize" ] "The boot files for SD image have been moved to the main ext4 partition. The FAT partition now only holds the Raspberry Pi firmware files. Changing its size may not be required.")
32 ../../profiles/all-hardware.nix
33 ];
34
35 options.sdImage = {
36 imageName = mkOption {
37 default = "${config.sdImage.imageBaseName}-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.img";
38 description = ''
39 Name of the generated image file.
40 '';
41 };
42
43 imageBaseName = mkOption {
44 default = "nixos-sd-image";
45 description = ''
46 Prefix of the name of the generated image file.
47 '';
48 };
49
50 storePaths = mkOption {
51 type = with types; listOf package;
52 example = literalExample "[ pkgs.stdenv ]";
53 description = ''
54 Derivations to be included in the Nix store in the generated SD image.
55 '';
56 };
57
58 firmwarePartitionID = mkOption {
59 type = types.str;
60 default = "0x2178694e";
61 description = ''
62 Volume ID for the /boot/firmware partition on the SD card. This value
63 must be a 32-bit hexadecimal number.
64 '';
65 };
66
67 firmwarePartitionName = mkOption {
68 type = types.str;
69 default = "FIRMWARE";
70 description = ''
71 Name of the filesystem which holds the boot firmware.
72 '';
73 };
74
75 rootPartitionUUID = mkOption {
76 type = types.nullOr types.str;
77 default = null;
78 example = "14e19a7b-0ae0-484d-9d54-43bd6fdc20c7";
79 description = ''
80 UUID for the filesystem on the main NixOS partition on the SD card.
81 '';
82 };
83
84 firmwareSize = mkOption {
85 type = types.int;
86 # As of 2019-08-18 the Raspberry pi firmware + u-boot takes ~18MiB
87 default = 30;
88 description = ''
89 Size of the /boot/firmware partition, in megabytes.
90 '';
91 };
92
93 populateFirmwareCommands = mkOption {
94 example = literalExample "'' cp \${pkgs.myBootLoader}/u-boot.bin firmware/ ''";
95 description = ''
96 Shell commands to populate the ./firmware directory.
97 All files in that directory are copied to the
98 /boot/firmware partition on the SD image.
99 '';
100 };
101
102 populateRootCommands = mkOption {
103 example = literalExample "''\${config.boot.loader.generic-extlinux-compatible.populateCmd} -c \${config.system.build.toplevel} -d ./files/boot''";
104 description = ''
105 Shell commands to populate the ./files directory.
106 All files in that directory are copied to the
107 root (/) partition on the SD image. Use this to
108 populate the ./files/boot (/boot) directory.
109 '';
110 };
111
112 postBuildCommands = mkOption {
113 example = literalExample "'' dd if=\${pkgs.myBootLoader}/SPL of=$img bs=1024 seek=1 conv=notrunc ''";
114 default = "";
115 description = ''
116 Shell commands to run after the image is built.
117 Can be used for boards requiring to dd u-boot SPL before actual partitions.
118 '';
119 };
120
121 compressImage = mkOption {
122 type = types.bool;
123 default = true;
124 description = ''
125 Whether the SD image should be compressed using
126 <command>zstd</command>.
127 '';
128 };
129
130 expandOnBoot = mkOption {
131 type = types.bool;
132 default = true;
133 description = ''
134 Whether to configure the sd image to expand it's partition on boot.
135 '';
136 };
137 };
138
139 config = {
140 fileSystems = {
141 "/boot/firmware" = {
142 device = "/dev/disk/by-label/${config.sdImage.firmwarePartitionName}";
143 fsType = "vfat";
144 # Alternatively, this could be removed from the configuration.
145 # The filesystem is not needed at runtime, it could be treated
146 # as an opaque blob instead of a discrete FAT32 filesystem.
147 options = [ "nofail" "noauto" ];
148 };
149 "/" = {
150 device = "/dev/disk/by-label/NIXOS_SD";
151 fsType = "ext4";
152 };
153 };
154
155 sdImage.storePaths = [ config.system.build.toplevel ];
156
157 system.build.sdImage = pkgs.callPackage ({ stdenv, dosfstools, e2fsprogs,
158 mtools, libfaketime, util-linux, zstd }: stdenv.mkDerivation {
159 name = config.sdImage.imageName;
160
161 nativeBuildInputs = [ dosfstools e2fsprogs mtools libfaketime util-linux zstd ];
162
163 inherit (config.sdImage) compressImage;
164
165 buildCommand = ''
166 mkdir -p $out/nix-support $out/sd-image
167 export img=$out/sd-image/${config.sdImage.imageName}
168
169 echo "${pkgs.stdenv.buildPlatform.system}" > $out/nix-support/system
170 if test -n "$compressImage"; then
171 echo "file sd-image $img.zst" >> $out/nix-support/hydra-build-products
172 else
173 echo "file sd-image $img" >> $out/nix-support/hydra-build-products
174 fi
175
176 echo "Decompressing rootfs image"
177 zstd -d --no-progress "${rootfsImage}" -o ./root-fs.img
178
179 # Gap in front of the first partition, in MiB
180 gap=8
181
182 # Create the image file sized to fit /boot/firmware and /, plus slack for the gap.
183 rootSizeBlocks=$(du -B 512 --apparent-size ./root-fs.img | awk '{ print $1 }')
184 firmwareSizeBlocks=$((${toString config.sdImage.firmwareSize} * 1024 * 1024 / 512))
185 imageSize=$((rootSizeBlocks * 512 + firmwareSizeBlocks * 512 + gap * 1024 * 1024))
186 truncate -s $imageSize $img
187
188 # type=b is 'W95 FAT32', type=83 is 'Linux'.
189 # The "bootable" partition is where u-boot will look file for the bootloader
190 # information (dtbs, extlinux.conf file).
191 sfdisk $img <<EOF
192 label: dos
193 label-id: ${config.sdImage.firmwarePartitionID}
194
195 start=''${gap}M, size=$firmwareSizeBlocks, type=b
196 start=$((gap + ${toString config.sdImage.firmwareSize}))M, type=83, bootable
197 EOF
198
199 # Copy the rootfs into the SD image
200 eval $(partx $img -o START,SECTORS --nr 2 --pairs)
201 dd conv=notrunc if=./root-fs.img of=$img seek=$START count=$SECTORS
202
203 # Create a FAT32 /boot/firmware partition of suitable size into firmware_part.img
204 eval $(partx $img -o START,SECTORS --nr 1 --pairs)
205 truncate -s $((SECTORS * 512)) firmware_part.img
206 faketime "1970-01-01 00:00:00" mkfs.vfat -i ${config.sdImage.firmwarePartitionID} -n ${config.sdImage.firmwarePartitionName} firmware_part.img
207
208 # Populate the files intended for /boot/firmware
209 mkdir firmware
210 ${config.sdImage.populateFirmwareCommands}
211
212 # Copy the populated /boot/firmware into the SD image
213 (cd firmware; mcopy -psvm -i ../firmware_part.img ./* ::)
214 # Verify the FAT partition before copying it.
215 fsck.vfat -vn firmware_part.img
216 dd conv=notrunc if=firmware_part.img of=$img seek=$START count=$SECTORS
217
218 ${config.sdImage.postBuildCommands}
219
220 if test -n "$compressImage"; then
221 zstd -T$NIX_BUILD_CORES --rm $img
222 fi
223 '';
224 }) {};
225
226 boot.postBootCommands = lib.mkIf config.sdImage.expandOnBoot ''
227 # On the first boot do some maintenance tasks
228 if [ -f /nix-path-registration ]; then
229 set -euo pipefail
230 set -x
231 # Figure out device names for the boot device and root filesystem.
232 rootPart=$(${pkgs.util-linux}/bin/findmnt -n -o SOURCE /)
233 bootDevice=$(lsblk -npo PKNAME $rootPart)
234 partNum=$(lsblk -npo MAJ:MIN $rootPart | ${pkgs.gawk}/bin/awk -F: '{print $2}')
235
236 # Resize the root partition and the filesystem to fit the disk
237 echo ",+," | sfdisk -N$partNum --no-reread $bootDevice
238 ${pkgs.parted}/bin/partprobe
239 ${pkgs.e2fsprogs}/bin/resize2fs $rootPart
240
241 # Register the contents of the initial Nix store
242 ${config.nix.package.out}/bin/nix-store --load-db < /nix-path-registration
243
244 # nixos-rebuild also requires a "system" profile and an /etc/NIXOS tag.
245 touch /etc/NIXOS
246 ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
247
248 # Prevents this from running on later boots.
249 rm -f /nix-path-registration
250 fi
251 '';
252 };
253}