diff --git a/flake.nix b/flake.nix index 72d6f2b..d6f5c7a 100644 --- a/flake.nix +++ b/flake.nix @@ -14,9 +14,9 @@ modules = [ ./base.nix ./kiosk.nix + ./sd-image rpi-nix.nixosModules.raspberry-pi - rpi-nix.nixosModules.sd-image nixos-hardware.nixosModules.raspberry-pi-4 ({ lib, pkgs, ... }: { sdImage.compressImage = false; diff --git a/sd-image/default.nix b/sd-image/default.nix new file mode 100644 index 0000000..b845de3 --- /dev/null +++ b/sd-image/default.nix @@ -0,0 +1,69 @@ +{ config, lib, pkgs, ... }: + +{ + imports = [ ./sd-image.nix ]; + + config = { + boot.loader.grub.enable = false; + + boot.consoleLogLevel = lib.mkDefault 7; + + boot.kernelParams = [ + # This is ugly and fragile, but the sdImage image has an msdos + # table, so the partition table id is a 1-indexed hex + # number. So, we drop the hex prefix and stick on a "02" to + # refer to the root partition. + "root=PARTUUID=${lib.strings.removePrefix "0x" config.sdImage.firmwarePartitionID}-02" + "rootfstype=ext4" + "fsck.repair=yes" + "rootwait" + ]; + + sdImage = + let + kernel-params = pkgs.writeTextFile { + name = "cmdline.txt"; + text = '' + ${lib.strings.concatStringsSep " " config.boot.kernelParams} + ''; + }; + cfg = config.raspberry-pi-nix; + version = cfg.kernel-version; + board = cfg.board; + kernel = "${config.system.build.kernel}/${config.system.boot.loader.kernelFile}"; + initrd = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}"; + populate-kernel = + if cfg.uboot.enable + then '' + cp ${cfg.uboot.package}/u-boot.bin firmware/u-boot-rpi-arm64.bin + '' + else '' + cp "${kernel}" firmware/kernel.img + cp "${initrd}" firmware/initrd + cp "${kernel-params}" firmware/cmdline.txt + ''; + in + { + populateFirmwareCommands = '' + ${populate-kernel} + cp -r ${pkgs.raspberrypifw}/share/raspberrypi/boot/{start*.elf,*.dtb,bootcode.bin,fixup*.dat,overlays} firmware + cp ${config.hardware.raspberry-pi.config-output} firmware/config.txt + ''; + populateRootCommands = + if cfg.uboot.enable + then '' + mkdir -p ./files/boot + ${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot + '' + else '' + mkdir -p ./files/sbin + content="$( + echo "#!${pkgs.bash}/bin/bash" + echo "exec ${config.system.build.toplevel}/init" + )" + echo "$content" > ./files/sbin/init + chmod 744 ./files/sbin/init + ''; + }; + }; +} diff --git a/sd-image/sd-image.nix b/sd-image/sd-image.nix new file mode 100644 index 0000000..cd6961d --- /dev/null +++ b/sd-image/sd-image.nix @@ -0,0 +1,270 @@ +# This module was lifted from nixpkgs installer code. It is modified +# so as to not import all-hardware. The goal here is to write the +# nixos image for a raspberry pi to an sd-card in a way so that we can +# pop it in and go. We don't need to support many possible hardware +# targets since we know we are targeting raspberry pi products. + +# This module creates a bootable SD card image containing the given NixOS +# configuration. The generated image is MBR partitioned, with a FAT +# /boot/firmware partition, and ext4 root partition. The generated image +# is sized to fit its contents, and a boot script automatically resizes +# the root partition to fit the device on the first boot. +# +# The firmware partition is built with expectation to hold the Raspberry +# Pi firmware and bootloader, and be removed and replaced with a firmware +# build for the target SoC for other board families. +# +# The derivation for the SD image will be placed in +# config.system.build.sdImage + +{ modulesPath, config, lib, pkgs, ... }: + +with lib; + +let + rootfsImage = pkgs.callPackage "${modulesPath}/../lib/make-ext4-fs.nix" ({ + inherit (config.sdImage) storePaths; + compressImage = false; + populateImageCommands = config.sdImage.populateRootCommands; + volumeLabel = "NIXOS_SD"; + } // optionalAttrs (config.sdImage.rootPartitionUUID != null) { + uuid = config.sdImage.rootPartitionUUID; + }); +in +{ + imports = [ ]; + + options.sdImage = { + imageName = mkOption { + default = + "${config.sdImage.imageBaseName}-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.img"; + description = '' + Name of the generated image file. + ''; + }; + + imageBaseName = mkOption { + default = "nixos-sd-image"; + description = '' + Prefix of the name of the generated image file. + ''; + }; + + storePaths = mkOption { + type = with types; listOf package; + example = literalExpression "[ pkgs.stdenv ]"; + description = '' + Derivations to be included in the Nix store in the generated SD image. + ''; + }; + + firmwarePartitionOffset = mkOption { + type = types.int; + default = 8; + description = '' + Gap in front of the /boot/firmware partition, in mebibytes (1024×1024 + bytes). + Can be increased to make more space for boards requiring to dd u-boot + SPL before actual partitions. + + Unless you are building your own images pre-configured with an + installed U-Boot, you can instead opt to delete the existing `FIRMWARE` + partition, which is used **only** for the Raspberry Pi family of + hardware. + ''; + }; + + firmwarePartitionID = mkOption { + type = types.str; + default = "0x2178694e"; + description = '' + Volume ID for the /boot/firmware partition on the SD card. This value + must be a 32-bit hexadecimal number. + ''; + }; + + rootPartitionUUID = mkOption { + type = types.nullOr types.str; + default = null; + example = "14e19a7b-0ae0-484d-9d54-43bd6fdc20c7"; + description = '' + UUID for the filesystem on the main NixOS partition on the SD card. + ''; + }; + + firmwareSize = mkOption { + type = types.int; + # As of 2019-08-18 the Raspberry pi firmware + u-boot takes ~18MiB + default = 128; + description = '' + Size of the /boot/firmware partition, in megabytes. + ''; + }; + + populateFirmwareCommands = mkOption { + example = + literalExpression "'' cp \${pkgs.myBootLoader}/u-boot.bin firmware/ ''"; + description = '' + Shell commands to populate the ./firmware directory. + All files in that directory are copied to the + /boot/firmware partition on the SD image. + ''; + }; + + populateRootCommands = mkOption { + example = literalExpression + "''\${config.boot.loader.generic-extlinux-compatible.populateCmd} -c \${config.system.build.toplevel} -d ./files/boot''"; + description = '' + Shell commands to populate the ./files directory. + All files in that directory are copied to the + root (/) partition on the SD image. Use this to + populate the ./files/boot (/boot) directory. + ''; + }; + + postBuildCommands = mkOption { + example = literalExpression + "'' dd if=\${pkgs.myBootLoader}/SPL of=$img bs=1024 seek=1 conv=notrunc ''"; + default = ""; + description = '' + Shell commands to run after the image is built. + Can be used for boards requiring to dd u-boot SPL before actual partitions. + ''; + }; + + compressImage = mkOption { + type = types.bool; + default = false; + description = '' + Whether the SD image should be compressed using + zstd. + ''; + }; + + expandOnBoot = mkOption { + type = types.bool; + default = true; + description = '' + Whether to configure the sd image to expand it's partition on boot. + ''; + }; + }; + + config = { + fileSystems = { + "/boot/firmware" = { + device = "/dev/disk/by-label/${config.raspberry-pi-nix.firmware-partition-label}"; + fsType = "vfat"; + }; + "/" = { + device = "/dev/disk/by-label/NIXOS_SD"; + fsType = "ext4"; + }; + }; + + sdImage.storePaths = [ config.system.build.toplevel ]; + + system.build.sdImage = pkgs.callPackage + ({ stdenv, dosfstools, e2fsprogs, mtools, libfaketime, util-linux, zstd }: + stdenv.mkDerivation { + name = config.sdImage.imageName; + + nativeBuildInputs = + [ dosfstools e2fsprogs mtools libfaketime util-linux zstd ]; + + inherit (config.sdImage) compressImage; + + buildCommand = '' + mkdir -p $out/nix-support $out/sd-image + export img=$out/sd-image/${config.sdImage.imageName} + + echo "${pkgs.stdenv.buildPlatform.system}" > $out/nix-support/system + if test -n "$compressImage"; then + echo "file sd-image $img.zst" >> $out/nix-support/hydra-build-products + else + echo "file sd-image $img" >> $out/nix-support/hydra-build-products + fi + + echo "Decompressing rootfs image" + cp "${rootfsImage}" ./root-fs.img + + # Gap in front of the first partition, in MiB + gap=${toString config.sdImage.firmwarePartitionOffset} + + # Create the image file sized to fit /boot/firmware and /, plus slack for the gap. + rootSizeBlocks=$(du -B 512 --apparent-size ./root-fs.img | awk '{ print $1 }') + firmwareSizeBlocks=$((${ + toString config.sdImage.firmwareSize + } * 1024 * 1024 / 512)) + imageSize=$((rootSizeBlocks * 512 + firmwareSizeBlocks * 512 + gap * 1024 * 1024)) + truncate -s $imageSize $img + + # type=b is 'W95 FAT32', type=83 is 'Linux'. + # The "bootable" partition is where u-boot will look file for the bootloader + # information (dtbs, extlinux.conf file). + sfdisk $img <