at 24.11-pre 11 kB view raw
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 = config.sdImage.compressImage; 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 = literalExpression "[ pkgs.stdenv ]"; 53 description = '' 54 Derivations to be included in the Nix store in the generated SD image. 55 ''; 56 }; 57 58 firmwarePartitionOffset = mkOption { 59 type = types.int; 60 default = 8; 61 description = '' 62 Gap in front of the /boot/firmware partition, in mebibytes (1024×1024 63 bytes). 64 Can be increased to make more space for boards requiring to dd u-boot 65 SPL before actual partitions. 66 67 Unless you are building your own images pre-configured with an 68 installed U-Boot, you can instead opt to delete the existing `FIRMWARE` 69 partition, which is used **only** for the Raspberry Pi family of 70 hardware. 71 ''; 72 }; 73 74 firmwarePartitionID = mkOption { 75 type = types.str; 76 default = "0x2178694e"; 77 description = '' 78 Volume ID for the /boot/firmware partition on the SD card. This value 79 must be a 32-bit hexadecimal number. 80 ''; 81 }; 82 83 firmwarePartitionName = mkOption { 84 type = types.str; 85 default = "FIRMWARE"; 86 description = '' 87 Name of the filesystem which holds the boot firmware. 88 ''; 89 }; 90 91 rootPartitionUUID = mkOption { 92 type = types.nullOr types.str; 93 default = null; 94 example = "14e19a7b-0ae0-484d-9d54-43bd6fdc20c7"; 95 description = '' 96 UUID for the filesystem on the main NixOS partition on the SD card. 97 ''; 98 }; 99 100 firmwareSize = mkOption { 101 type = types.int; 102 # As of 2019-08-18 the Raspberry pi firmware + u-boot takes ~18MiB 103 default = 30; 104 description = '' 105 Size of the /boot/firmware partition, in megabytes. 106 ''; 107 }; 108 109 populateFirmwareCommands = mkOption { 110 example = literalExpression "'' cp \${pkgs.myBootLoader}/u-boot.bin firmware/ ''"; 111 description = '' 112 Shell commands to populate the ./firmware directory. 113 All files in that directory are copied to the 114 /boot/firmware partition on the SD image. 115 ''; 116 }; 117 118 populateRootCommands = mkOption { 119 example = literalExpression "''\${config.boot.loader.generic-extlinux-compatible.populateCmd} -c \${config.system.build.toplevel} -d ./files/boot''"; 120 description = '' 121 Shell commands to populate the ./files directory. 122 All files in that directory are copied to the 123 root (/) partition on the SD image. Use this to 124 populate the ./files/boot (/boot) directory. 125 ''; 126 }; 127 128 postBuildCommands = mkOption { 129 example = literalExpression "'' dd if=\${pkgs.myBootLoader}/SPL of=$img bs=1024 seek=1 conv=notrunc ''"; 130 default = ""; 131 description = '' 132 Shell commands to run after the image is built. 133 Can be used for boards requiring to dd u-boot SPL before actual partitions. 134 ''; 135 }; 136 137 compressImage = mkOption { 138 type = types.bool; 139 default = true; 140 description = '' 141 Whether the SD image should be compressed using 142 {command}`zstd`. 143 ''; 144 }; 145 146 expandOnBoot = mkOption { 147 type = types.bool; 148 default = true; 149 description = '' 150 Whether to configure the sd image to expand it's partition on boot. 151 ''; 152 }; 153 }; 154 155 config = { 156 fileSystems = { 157 "/boot/firmware" = { 158 device = "/dev/disk/by-label/${config.sdImage.firmwarePartitionName}"; 159 fsType = "vfat"; 160 # Alternatively, this could be removed from the configuration. 161 # The filesystem is not needed at runtime, it could be treated 162 # as an opaque blob instead of a discrete FAT32 filesystem. 163 options = [ "nofail" "noauto" ]; 164 }; 165 "/" = { 166 device = "/dev/disk/by-label/NIXOS_SD"; 167 fsType = "ext4"; 168 }; 169 }; 170 171 sdImage.storePaths = [ config.system.build.toplevel ]; 172 173 system.build.sdImage = pkgs.callPackage ({ stdenv, dosfstools, e2fsprogs, 174 mtools, libfaketime, util-linux, zstd }: stdenv.mkDerivation { 175 name = config.sdImage.imageName; 176 177 nativeBuildInputs = [ dosfstools e2fsprogs libfaketime mtools util-linux ] 178 ++ lib.optional config.sdImage.compressImage zstd; 179 180 inherit (config.sdImage) imageName compressImage; 181 182 buildCommand = '' 183 mkdir -p $out/nix-support $out/sd-image 184 export img=$out/sd-image/${config.sdImage.imageName} 185 186 echo "${pkgs.stdenv.buildPlatform.system}" > $out/nix-support/system 187 if test -n "$compressImage"; then 188 echo "file sd-image $img.zst" >> $out/nix-support/hydra-build-products 189 else 190 echo "file sd-image $img" >> $out/nix-support/hydra-build-products 191 fi 192 193 root_fs=${rootfsImage} 194 ${lib.optionalString config.sdImage.compressImage '' 195 root_fs=./root-fs.img 196 echo "Decompressing rootfs image" 197 zstd -d --no-progress "${rootfsImage}" -o $root_fs 198 ''} 199 200 # Gap in front of the first partition, in MiB 201 gap=${toString config.sdImage.firmwarePartitionOffset} 202 203 # Create the image file sized to fit /boot/firmware and /, plus slack for the gap. 204 rootSizeBlocks=$(du -B 512 --apparent-size $root_fs | awk '{ print $1 }') 205 firmwareSizeBlocks=$((${toString config.sdImage.firmwareSize} * 1024 * 1024 / 512)) 206 imageSize=$((rootSizeBlocks * 512 + firmwareSizeBlocks * 512 + gap * 1024 * 1024)) 207 truncate -s $imageSize $img 208 209 # type=b is 'W95 FAT32', type=83 is 'Linux'. 210 # The "bootable" partition is where u-boot will look file for the bootloader 211 # information (dtbs, extlinux.conf file). 212 sfdisk $img <<EOF 213 label: dos 214 label-id: ${config.sdImage.firmwarePartitionID} 215 216 start=''${gap}M, size=$firmwareSizeBlocks, type=b 217 start=$((gap + ${toString config.sdImage.firmwareSize}))M, type=83, bootable 218 EOF 219 220 # Copy the rootfs into the SD image 221 eval $(partx $img -o START,SECTORS --nr 2 --pairs) 222 dd conv=notrunc if=$root_fs of=$img seek=$START count=$SECTORS 223 224 # Create a FAT32 /boot/firmware partition of suitable size into firmware_part.img 225 eval $(partx $img -o START,SECTORS --nr 1 --pairs) 226 truncate -s $((SECTORS * 512)) firmware_part.img 227 228 mkfs.vfat --invariant -i ${config.sdImage.firmwarePartitionID} -n ${config.sdImage.firmwarePartitionName} firmware_part.img 229 230 # Populate the files intended for /boot/firmware 231 mkdir firmware 232 ${config.sdImage.populateFirmwareCommands} 233 234 find firmware -exec touch --date=2000-01-01 {} + 235 # Copy the populated /boot/firmware into the SD image 236 cd firmware 237 # Force a fixed order in mcopy for better determinism, and avoid file globbing 238 for d in $(find . -type d -mindepth 1 | sort); do 239 faketime "2000-01-01 00:00:00" mmd -i ../firmware_part.img "::/$d" 240 done 241 for f in $(find . -type f | sort); do 242 mcopy -pvm -i ../firmware_part.img "$f" "::/$f" 243 done 244 cd .. 245 246 # Verify the FAT partition before copying it. 247 fsck.vfat -vn firmware_part.img 248 dd conv=notrunc if=firmware_part.img of=$img seek=$START count=$SECTORS 249 250 ${config.sdImage.postBuildCommands} 251 252 if test -n "$compressImage"; then 253 zstd -T$NIX_BUILD_CORES --rm $img 254 fi 255 ''; 256 }) {}; 257 258 boot.postBootCommands = lib.mkIf config.sdImage.expandOnBoot '' 259 # On the first boot do some maintenance tasks 260 if [ -f /nix-path-registration ]; then 261 set -euo pipefail 262 set -x 263 # Figure out device names for the boot device and root filesystem. 264 rootPart=$(${pkgs.util-linux}/bin/findmnt -n -o SOURCE /) 265 bootDevice=$(lsblk -npo PKNAME $rootPart) 266 partNum=$(lsblk -npo MAJ:MIN $rootPart | ${pkgs.gawk}/bin/awk -F: '{print $2}') 267 268 # Resize the root partition and the filesystem to fit the disk 269 echo ",+," | sfdisk -N$partNum --no-reread $bootDevice 270 ${pkgs.parted}/bin/partprobe 271 ${pkgs.e2fsprogs}/bin/resize2fs $rootPart 272 273 # Register the contents of the initial Nix store 274 ${config.nix.package.out}/bin/nix-store --load-db < /nix-path-registration 275 276 # nixos-rebuild also requires a "system" profile and an /etc/NIXOS tag. 277 touch /etc/NIXOS 278 ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system 279 280 # Prevents this from running on later boots. 281 rm -f /nix-path-registration 282 fi 283 ''; 284 }; 285}