at master 12 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{ 15 config, 16 lib, 17 pkgs, 18 ... 19}: 20 21with lib; 22 23let 24 rootfsImage = pkgs.callPackage ../../../lib/make-ext4-fs.nix ( 25 { 26 inherit (config.sdImage) storePaths; 27 compressImage = config.sdImage.compressImage; 28 populateImageCommands = config.sdImage.populateRootCommands; 29 volumeLabel = config.sdImage.rootVolumeLabel; 30 } 31 // optionalAttrs (config.sdImage.rootPartitionUUID != null) { 32 uuid = config.sdImage.rootPartitionUUID; 33 } 34 ); 35in 36{ 37 imports = [ 38 (mkRemovedOptionModule [ "sdImage" "bootPartitionID" ] 39 "The FAT partition for SD image now only holds the Raspberry Pi firmware files. Use firmwarePartitionID to configure that partition's ID." 40 ) 41 (mkRemovedOptionModule [ "sdImage" "bootSize" ] 42 "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." 43 ) 44 (lib.mkRenamedOptionModuleWith { 45 sinceRelease = 2505; 46 from = [ 47 "sdImage" 48 "imageBaseName" 49 ]; 50 to = [ 51 "image" 52 "baseName" 53 ]; 54 }) 55 (lib.mkRenamedOptionModuleWith { 56 sinceRelease = 2505; 57 from = [ 58 "sdImage" 59 "imageName" 60 ]; 61 to = [ 62 "image" 63 "fileName" 64 ]; 65 }) 66 ../../profiles/all-hardware.nix 67 ../../image/file-options.nix 68 ]; 69 70 options.sdImage = { 71 storePaths = mkOption { 72 type = with types; listOf package; 73 example = literalExpression "[ pkgs.stdenv ]"; 74 description = '' 75 Derivations to be included in the Nix store in the generated SD image. 76 ''; 77 }; 78 79 firmwarePartitionOffset = mkOption { 80 type = types.int; 81 default = 8; 82 description = '' 83 Gap in front of the /boot/firmware partition, in MiB (1024×1024 bytes). 84 Can be increased to make more space for boards requiring to dd u-boot 85 SPL before actual partitions. 86 87 Unless you are building your own images pre-configured with an 88 installed U-Boot, you can instead opt to delete the existing `FIRMWARE` 89 partition, which is used **only** for the Raspberry Pi family of 90 hardware. 91 ''; 92 }; 93 94 firmwarePartitionID = mkOption { 95 type = types.str; 96 default = "0x2178694e"; 97 description = '' 98 Volume ID for the /boot/firmware partition on the SD card. This value 99 must be a 32-bit hexadecimal number. 100 ''; 101 }; 102 103 firmwarePartitionName = mkOption { 104 type = types.str; 105 default = "FIRMWARE"; 106 description = '' 107 Name of the filesystem which holds the boot firmware. 108 ''; 109 }; 110 111 rootPartitionUUID = mkOption { 112 type = types.nullOr types.str; 113 default = null; 114 example = "14e19a7b-0ae0-484d-9d54-43bd6fdc20c7"; 115 description = '' 116 UUID for the filesystem on the main NixOS partition on the SD card. 117 ''; 118 }; 119 120 rootVolumeLabel = mkOption { 121 type = types.str; 122 default = "NIXOS_SD"; 123 example = "NIXOS_PENDRIVE"; 124 description = '' 125 Label for the NixOS root volume. 126 Usually used when creating a recovery NixOS media installation 127 that avoids conflicting with previous instalation label. 128 ''; 129 }; 130 131 firmwareSize = mkOption { 132 type = types.int; 133 # As of 2019-08-18 the Raspberry pi firmware + u-boot takes ~18MiB 134 default = 30; 135 description = '' 136 Size of the /boot/firmware partition, in megabytes. 137 ''; 138 }; 139 140 populateFirmwareCommands = mkOption { 141 example = literalExpression "'' cp \${pkgs.myBootLoader}/u-boot.bin firmware/ ''"; 142 description = '' 143 Shell commands to populate the ./firmware directory. 144 All files in that directory are copied to the 145 /boot/firmware partition on the SD image. 146 ''; 147 }; 148 149 populateRootCommands = mkOption { 150 example = literalExpression "''\${config.boot.loader.generic-extlinux-compatible.populateCmd} -c \${config.system.build.toplevel} -d ./files/boot''"; 151 description = '' 152 Shell commands to populate the ./files directory. 153 All files in that directory are copied to the 154 root (/) partition on the SD image. Use this to 155 populate the ./files/boot (/boot) directory. 156 ''; 157 }; 158 159 postBuildCommands = mkOption { 160 example = literalExpression "'' dd if=\${pkgs.myBootLoader}/SPL of=$img bs=1024 seek=1 conv=notrunc ''"; 161 default = ""; 162 description = '' 163 Shell commands to run after the image is built. 164 Can be used for boards requiring to dd u-boot SPL before actual partitions. 165 ''; 166 }; 167 168 compressImage = mkOption { 169 type = types.bool; 170 default = true; 171 description = '' 172 Whether the SD image should be compressed using 173 {command}`zstd`. 174 ''; 175 }; 176 177 expandOnBoot = mkOption { 178 type = types.bool; 179 default = true; 180 description = '' 181 Whether to configure the sd image to expand it's partition on boot. 182 ''; 183 }; 184 185 nixPathRegistrationFile = mkOption { 186 type = types.str; 187 default = "/nix-path-registration"; 188 description = '' 189 Location of the file containing the input for nix-store --load-db once the machine has booted. 190 If overriding fileSystems."/" then you should to set this to the root mount + /nix-path-registration 191 ''; 192 }; 193 }; 194 195 config = { 196 hardware.enableAllHardware = true; 197 198 fileSystems = { 199 "/boot/firmware" = { 200 device = "/dev/disk/by-label/${config.sdImage.firmwarePartitionName}"; 201 fsType = "vfat"; 202 # Alternatively, this could be removed from the configuration. 203 # The filesystem is not needed at runtime, it could be treated 204 # as an opaque blob instead of a discrete FAT32 filesystem. 205 options = [ 206 "nofail" 207 "noauto" 208 ]; 209 }; 210 "/" = { 211 device = "/dev/disk/by-label/${config.sdImage.rootVolumeLabel}"; 212 fsType = "ext4"; 213 }; 214 }; 215 216 sdImage.storePaths = [ config.system.build.toplevel ]; 217 218 image.extension = if config.sdImage.compressImage then "img.zst" else "img"; 219 image.filePath = "sd-image/${config.image.fileName}"; 220 system.nixos.tags = [ "sd-card" ]; 221 system.build.image = config.system.build.sdImage; 222 system.build.sdImage = pkgs.callPackage ( 223 { 224 stdenv, 225 dosfstools, 226 e2fsprogs, 227 mtools, 228 libfaketime, 229 util-linux, 230 zstd, 231 }: 232 stdenv.mkDerivation { 233 name = config.image.fileName; 234 235 nativeBuildInputs = [ 236 dosfstools 237 e2fsprogs 238 libfaketime 239 mtools 240 util-linux 241 ] 242 ++ lib.optional config.sdImage.compressImage zstd; 243 244 inherit (config.sdImage) compressImage; 245 246 buildCommand = '' 247 mkdir -p $out/nix-support $out/sd-image 248 export img=$out/sd-image/${config.image.baseName}.img 249 250 echo "${pkgs.stdenv.buildPlatform.system}" > $out/nix-support/system 251 if test -n "$compressImage"; then 252 echo "file sd-image $img.zst" >> $out/nix-support/hydra-build-products 253 else 254 echo "file sd-image $img" >> $out/nix-support/hydra-build-products 255 fi 256 257 root_fs=${rootfsImage} 258 ${lib.optionalString config.sdImage.compressImage '' 259 root_fs=./root-fs.img 260 echo "Decompressing rootfs image" 261 zstd -d --no-progress "${rootfsImage}" -o $root_fs 262 ''} 263 264 # Gap in front of the first partition, in MiB 265 gap=${toString config.sdImage.firmwarePartitionOffset} 266 267 # Create the image file sized to fit /boot/firmware and /, plus slack for the gap. 268 rootSizeBlocks=$(du -B 512 --apparent-size $root_fs | awk '{ print $1 }') 269 firmwareSizeBlocks=$((${toString config.sdImage.firmwareSize} * 1024 * 1024 / 512)) 270 imageSize=$((rootSizeBlocks * 512 + firmwareSizeBlocks * 512 + gap * 1024 * 1024)) 271 truncate -s $imageSize $img 272 273 # type=b is 'W95 FAT32', type=83 is 'Linux'. 274 # The "bootable" partition is where u-boot will look file for the bootloader 275 # information (dtbs, extlinux.conf file). 276 sfdisk --no-reread --no-tell-kernel $img <<EOF 277 label: dos 278 label-id: ${config.sdImage.firmwarePartitionID} 279 280 start=''${gap}M, size=$firmwareSizeBlocks, type=b 281 start=$((gap + ${toString config.sdImage.firmwareSize}))M, type=83, bootable 282 EOF 283 284 # Copy the rootfs into the SD image 285 eval $(partx $img -o START,SECTORS --nr 2 --pairs) 286 dd conv=notrunc if=$root_fs of=$img seek=$START count=$SECTORS 287 288 # Create a FAT32 /boot/firmware partition of suitable size into firmware_part.img 289 eval $(partx $img -o START,SECTORS --nr 1 --pairs) 290 truncate -s $((SECTORS * 512)) firmware_part.img 291 292 mkfs.vfat --invariant -i ${config.sdImage.firmwarePartitionID} -n ${config.sdImage.firmwarePartitionName} firmware_part.img 293 294 # Populate the files intended for /boot/firmware 295 mkdir firmware 296 ${config.sdImage.populateFirmwareCommands} 297 298 find firmware -exec touch --date=2000-01-01 {} + 299 # Copy the populated /boot/firmware into the SD image 300 cd firmware 301 # Force a fixed order in mcopy for better determinism, and avoid file globbing 302 for d in $(find . -type d -mindepth 1 | sort); do 303 faketime "2000-01-01 00:00:00" mmd -i ../firmware_part.img "::/$d" 304 done 305 for f in $(find . -type f | sort); do 306 mcopy -pvm -i ../firmware_part.img "$f" "::/$f" 307 done 308 cd .. 309 310 # Verify the FAT partition before copying it. 311 fsck.vfat -vn firmware_part.img 312 dd conv=notrunc if=firmware_part.img of=$img seek=$START count=$SECTORS 313 314 ${config.sdImage.postBuildCommands} 315 316 if test -n "$compressImage"; then 317 zstd -T$NIX_BUILD_CORES --rm $img 318 fi 319 ''; 320 } 321 ) { }; 322 323 boot.postBootCommands = 324 let 325 expandOnBoot = lib.optionalString config.sdImage.expandOnBoot '' 326 # Figure out device names for the boot device and root filesystem. 327 rootPart=$(${pkgs.util-linux}/bin/findmnt -n -o SOURCE /) 328 bootDevice=$(lsblk -npo PKNAME $rootPart) 329 partNum=$(lsblk -npo MAJ:MIN $rootPart | ${pkgs.gawk}/bin/awk -F: '{print $2}') 330 331 # Resize the root partition and the filesystem to fit the disk 332 echo ",+," | sfdisk -N$partNum --no-reread $bootDevice 333 ${pkgs.parted}/bin/partprobe 334 ${pkgs.e2fsprogs}/bin/resize2fs $rootPart 335 ''; 336 nixPathRegistrationFile = config.sdImage.nixPathRegistrationFile; 337 in 338 '' 339 # On the first boot do some maintenance tasks 340 if [ -f ${nixPathRegistrationFile} ]; then 341 set -euo pipefail 342 set -x 343 344 ${expandOnBoot} 345 346 # Register the contents of the initial Nix store 347 ${config.nix.package.out}/bin/nix-store --load-db < ${nixPathRegistrationFile} 348 349 # nixos-rebuild also requires a "system" profile and an /etc/NIXOS tag. 350 touch /etc/NIXOS 351 ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system 352 353 # Prevents this from running on later boots. 354 rm -f ${nixPathRegistrationFile} 355 fi 356 ''; 357 }; 358}