nixos: Add derivations for SD card installation images on ARM

The resulting image can be copied to a SD card with `dd` and is directly
bootable by a suitably configured U-Boot. Though depending on the board, some
extra steps are required for copying U-Boot itself to the SD card.

Inside the image is a partition table, with a FAT32 /boot and a normal
writable EXT4 rootfs. It's possible to directly reuse the SD image's
partition layout and "install" NixOS on the same SD card by replacing
the default configuration.nix and nixos-rebuild, and actually is the
preferred way to use these images. To assist in this installation
method, the boot scripts on the image automatically resize the rootfs
partition to fit the SD card on the first boot.

The SD images come in two flavors; one for the ARMv6 Raspberry Pi,
and one multiplatform image for all the boards supported by the
mainline kernel's multi_v7_defconfig config target. At the moment, these
have been tested on:
- Raspberry Pi Model B (512MB model)
- NVIDIA Jetson TK1
- Linksprite pcDuino3 Nano

To build, run:

nix-build '<nixpkgs/nixos>' -A config.system.build.sdImage \
-I nixos-config='<nixpkgs/nixos/modules/installer/cd-dvd/sd-image-armv7l-multiplatform.nix>'

+301
+88
nixos/lib/make-ext4-fs.nix
··· 1 + # Builds an ext4 image containing a populated /nix/store with the closure 2 + # of store paths passed in the storePaths parameter. The generated image 3 + # is sized to only fit its contents, with the expectation that a script 4 + # resizes the filesystem at boot time. 5 + { pkgs 6 + , storePaths 7 + , volumeLabel 8 + }: 9 + 10 + pkgs.stdenv.mkDerivation { 11 + name = "ext4-fs.img"; 12 + 13 + buildInputs = with pkgs; [e2fsprogs libfaketime perl]; 14 + 15 + # For obtaining the closure of `storePaths'. 16 + exportReferencesGraph = 17 + map (x: [("closure-" + baseNameOf x) x]) storePaths; 18 + 19 + buildCommand = 20 + '' 21 + # Add the closures of the top-level store objects. 22 + storePaths=$(perl ${pkgs.pathsFromGraph} closure-*) 23 + 24 + # Also include a manifest of the closures in a format suitable 25 + # for nix-store --load-db. 26 + printRegistration=1 perl ${pkgs.pathsFromGraph} closure-* > nix-path-registration 27 + 28 + # Make a crude approximation of the size of the target image. 29 + # If the script starts failing, increase the fudge factors here. 30 + numInodes=$(find $storePaths | wc -l) 31 + numDataBlocks=$(du -c -B 4096 --apparent-size $storePaths | awk '$2 == "total" { print int($1 * 1.03) }') 32 + bytes=$((2 * 4096 * $numInodes + 4096 * $numDataBlocks)) 33 + echo "Creating an EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks)" 34 + 35 + truncate -s $bytes $out 36 + faketime "1970-01-01 00:00:00" mkfs.ext4 -L ${volumeLabel} -U 44444444-4444-4444-8888-888888888888 $out 37 + 38 + # Populate the image contents by piping a bunch of commands to the `debugfs` tool from e2fsprogs. 39 + # For example, to copy /nix/store/abcd...efg-coreutils-8.23/bin/sleep: 40 + # cd /nix/store/abcd...efg-coreutils-8.23/bin 41 + # write /nix/store/abcd...efg-coreutils-8.23/bin/sleep sleep 42 + # sif sleep mode 040555 43 + # sif sleep gid 30000 44 + # In particular, debugfs doesn't handle absolute target paths; you have to 'cd' in the virtual 45 + # filesystem first. Likewise the intermediate directories must already exist (using `find` 46 + # handles that for us). And when setting the file's permissions, the inode type flags (__S_IFDIR, 47 + # __S_IFREG) need to be set as well. 48 + ( 49 + echo write nix-path-registration nix-path-registration 50 + echo mkdir nix 51 + echo cd /nix 52 + echo mkdir store 53 + 54 + # XXX: This explodes in exciting ways if anything in /nix/store has a space in it. 55 + find $storePaths -printf '%y %f %h %m\n'| while read -r type file dir perms; do 56 + # echo "TYPE=$type DIR=$dir FILE=$file PERMS=$perms" >&2 57 + 58 + echo "cd $dir" 59 + case $type in 60 + d) 61 + echo "mkdir $file" 62 + echo sif $file mode $((040000 | 0$perms)) # magic constant is __S_IFDIR 63 + ;; 64 + f) 65 + echo "write $dir/$file $file" 66 + echo sif $file mode $((0100000 | 0$perms)) # magic constant is __S_IFREG 67 + ;; 68 + l) 69 + echo "symlink $file $(readlink "$dir/$file")" 70 + ;; 71 + *) 72 + echo "Unknown entry: $type $dir $file $perms" >&2 73 + exit 1 74 + ;; 75 + esac 76 + 77 + echo sif $file gid 30000 # chgrp to nixbld 78 + done 79 + ) | faketime "1970-01-01 00:00:00" debugfs -w $out -f /dev/stdin > errorlog 2>&1 80 + 81 + # The debugfs tool doesn't terminate on error nor exit with a non-zero status. Check manually. 82 + if egrep -q 'Could not allocate|File not found' errorlog; then 83 + cat errorlog 84 + echo "--- Failed to create EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks) ---" 85 + return 1 86 + fi 87 + ''; 88 + }
+40
nixos/modules/installer/cd-dvd/sd-image-armv7l-multiplatform.nix
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + let 4 + extlinux-conf-builder = 5 + import ../../system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.nix { 6 + inherit pkgs; 7 + }; 8 + in 9 + { 10 + imports = [ 11 + ../../profiles/minimal.nix 12 + ../../profiles/installation-device.nix 13 + ./sd-image.nix 14 + ]; 15 + 16 + assertions = lib.singleton { 17 + assertion = pkgs.stdenv.system == "armv7l-linux"; 18 + message = "sd-image-armv7l-multiplatform.nix can be only built natively on ARMv7; " + 19 + "it cannot be cross compiled"; 20 + }; 21 + 22 + boot.loader.grub.enable = false; 23 + boot.loader.generic-extlinux-compatible.enable = true; 24 + 25 + # FIXME: change this to linuxPackages_latest once v4.2 is out 26 + boot.kernelPackages = pkgs.linuxPackages_testing; 27 + boot.kernelParams = ["console=ttyS0,115200n8" "console=ttyAMA0,115200n8" "console=tty0"]; 28 + 29 + # FIXME: fix manual evaluation on ARM 30 + services.nixosManual.enable = lib.mkOverride 0 false; 31 + 32 + # FIXME: this probably should be in installation-device.nix 33 + users.extraUsers.root.initialHashedPassword = ""; 34 + 35 + sdImage = { 36 + populateBootCommands = '' 37 + ${extlinux-conf-builder} -t 3 -c ${config.system.build.toplevel} -d ./boot 38 + ''; 39 + }; 40 + }
+46
nixos/modules/installer/cd-dvd/sd-image-raspberrypi.nix
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + let 4 + extlinux-conf-builder = 5 + import ../../system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.nix { 6 + inherit pkgs; 7 + }; 8 + in 9 + { 10 + imports = [ 11 + ../../profiles/minimal.nix 12 + ../../profiles/installation-device.nix 13 + ./sd-image.nix 14 + ]; 15 + 16 + assertions = lib.singleton { 17 + assertion = pkgs.stdenv.system == "armv6l-linux"; 18 + message = "sd-image-raspberrypi.nix can be only built natively on ARMv6; " + 19 + "it cannot be cross compiled"; 20 + }; 21 + 22 + # Needed by RPi firmware 23 + nixpkgs.config.allowUnfree = true; 24 + 25 + boot.loader.grub.enable = false; 26 + boot.loader.generic-extlinux-compatible.enable = true; 27 + 28 + boot.kernelPackages = pkgs.linuxPackages_rpi; 29 + 30 + # FIXME: fix manual evaluation on ARM 31 + services.nixosManual.enable = lib.mkOverride 0 false; 32 + 33 + # FIXME: this probably should be in installation-device.nix 34 + users.extraUsers.root.initialHashedPassword = ""; 35 + 36 + sdImage = { 37 + populateBootCommands = '' 38 + for f in bootcode.bin fixup.dat start.elf; do 39 + cp ${pkgs.raspberrypifw}/share/raspberrypi/boot/$f boot/ 40 + done 41 + cp ${pkgs.ubootRaspberryPi}/u-boot.bin boot/u-boot-rpi.bin 42 + echo 'kernel u-boot-rpi.bin' > boot/config.txt 43 + ${extlinux-conf-builder} -t 3 -c ${config.system.build.toplevel} -d ./boot 44 + ''; 45 + }; 46 + }
+127
nixos/modules/installer/cd-dvd/sd-image.nix
··· 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 + 12 + with lib; 13 + 14 + let 15 + rootfsImage = import ../../../lib/make-ext4-fs.nix { 16 + inherit pkgs; 17 + inherit (config.sdImage) storePaths; 18 + volumeLabel = "NIXOS_SD"; 19 + }; 20 + in 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 = 128; 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 4M 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 + 4096 * 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=1M, size=$bootSizeBlocks, type=b, bootable 81 + 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}/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}/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 + }