From 3d7a565149d976431cc8b3fdc05f46314b5bfe04 Mon Sep 17 00:00:00 2001 From: Travis Staton Date: Wed, 19 Jan 2022 21:35:25 -0500 Subject: [PATCH] init --- README.md | 102 ++++++++++++++ flake.lock | 27 ++++ flake.nix | 16 +++ overlay/default.nix | 51 +++++++ overlay/libcamera-apps.nix | 33 +++++ overlay/libcamera.patch | 19 +++ rpi-3b-plus/default.nix | 15 ++ rpi-4b/default.nix | 8 ++ rpi-zero-2-w/default.nix | 13 ++ rpi/audio.nix | 17 +++ rpi/default.nix | 26 ++++ rpi/device-tree.nix | 97 +++++++++++++ rpi/i2c.nix | 14 ++ rpi/i2s.nix | 13 ++ rpi/modesetting.nix | 23 +++ sd-image/default.nix | 76 ++++++++++ sd-image/sd-image.nix | 281 +++++++++++++++++++++++++++++++++++++ 17 files changed, 831 insertions(+) create mode 100644 README.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 overlay/default.nix create mode 100644 overlay/libcamera-apps.nix create mode 100644 overlay/libcamera.patch create mode 100644 rpi-3b-plus/default.nix create mode 100644 rpi-4b/default.nix create mode 100644 rpi-zero-2-w/default.nix create mode 100644 rpi/audio.nix create mode 100644 rpi/default.nix create mode 100644 rpi/device-tree.nix create mode 100644 rpi/i2c.nix create mode 100644 rpi/i2s.nix create mode 100644 rpi/modesetting.nix create mode 100644 sd-image/default.nix create mode 100644 sd-image/sd-image.nix diff --git a/README.md b/README.md new file mode 100644 index 0000000..b764881 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# raspberry-pi-nix + +NixOS modules that make building images for raspberry-pi products +easier. Most of the work in this repository is based on work in +[nixos-hardware](https://github.com/NixOS/nixos-hardware) and +[nixpkgs](https://github.com/NixOS/nixpkgs). Additionally, be aware +that I am no expert and this repo is the product of me fooling around +with some pis. + +This flake provides nixos modules that correspond to different +raspberry-pi products. These modules can be included in nixos +configurations and aim to deliver the following benefits: + +1. Configure the kernel, device tree, and u-boot in a way that is + compatible with the hardware. +2. Provide a nix interface to device tree configuration that will be + familiar to those who have used raspberry-pi's config.txt based + configuration. +3. Make it easy to build an image suitable for flashing to an sd-card, + without a need to first go through an installation media. + +The important modules are `overlay/default.nix`, `rpi/default.nix`, +and `rpi/device-tree.nix`. The other modules for i2c, i2s, etc are +mostly wrappers that set common device tree settings for you. + +## Example + +```nix +{ + description = "raspberry-pi-nix example"; + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-21.11"; + raspberry-pi-nix = { + url = "github:tstat/raspberry-pi-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, raspberry-pi-nix }: + let + inherit (nixpkgs.lib) nixosSystem; + basic-config = { pkgs, lib, ... }: { + time.timeZone = "America/New_York"; + users.users.root.initialPassword = "root"; + networking = { + hostName = "basic-example"; + useDHCP = false; + interfaces = { wlan0.useDHCP = true; }; + }; + hardware.raspberry-pi = { + i2c.enable = true; + audio.enable = true; + fkms-3d.enable = true; + deviceTree = { + dt-overlays = [{ + overlay = "imx477"; # add the overlay for the HQ camera + args = [ ]; + }]; + }; + }; + }; + in { + nixosConfigurations = { + rpi-zero-2-w-example = nixosSystem { + system = "aarch64-linux"; + modules = [ raspberry-pi-nix.rpi-zero-2-w basic-config ]; + }; + rpi-4b-example = nixosSystem { + system = "aarch64-linux"; + modules = [ raspberry-pi-nix.rpi-4b basic-config ]; + }; + }; + }; +} +``` + +## Building an sd-card image + +An image suitable for flashing to an sd-card can be found at the +attribute `config.system.build.sdImage`. For example, if you wanted to +build an image for `rpi-zero-2-w-example` in the above configuration +example you could run: + +``` +nix build '.#nixosConfigurations.rpi-zero-2-w-example.config.system.build.sdImage' +``` + +## Other notes + +The sd-image built is partitioned in the same way as the aarch64 +installation media from nixpkgs: There is a firmware partition that +contains necessary firmware, u-boot, and config.txt. Then there is +another partition that contains everything else. After the sd-image is +built, nixos system updates will not change anything in the firmware +partition ever again. New kernels and device tree configurations will +remain on the nixos partition and be booted by u-boot in the firmware +partition. + +So, while you can control device tree params and overlays through your +nixos system configuration, if you want to modify other config.txt +variables this must be done manually by mounting the partition and +modifying the config.txt file. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..b954c28 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1652182392, + "narHash": "sha256-H9Bmor+kfogrE0X7Fi5sh0gCUWDG4pnmYxedJyIT41A=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "aa2f845096f72dde4ad0c168eeec387cbd2eae04", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-21.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..0e4fe7e --- /dev/null +++ b/flake.nix @@ -0,0 +1,16 @@ +{ + description = "raspberry-pi nixos configuration"; + + inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-21.11"; }; + + outputs = { self, nixpkgs }: { + overlay = import ./overlay; + rpi = import ./rpi { + nixpkgs = nixpkgs; + overlay = self.overlay; + }; + rpi-3b-plus = import ./rpi-3b-plus self.rpi; + rpi-4b = import ./rpi-4b self.rpi; + rpi-zero-2-w = import ./rpi-zero-2-w self.rpi; + }; +} diff --git a/overlay/default.nix b/overlay/default.nix new file mode 100644 index 0000000..fb66e97 --- /dev/null +++ b/overlay/default.nix @@ -0,0 +1,51 @@ +final: prev: { + # newer version of libcamera + libcamera = prev.libcamera.overrideAttrs (old: { + src = prev.fetchgit { + url = "https://git.libcamera.org/libcamera/libcamera.git"; + rev = "44d59841e1ce59042b8069b8078bc9f7b1bfa73b"; + sha256 = "1nzkvy2y772ak9gax456ws2fmjc9ncams0m1w27h1rzpxn5yphqr"; + }; + mesonFlags = [ "-Dv4l2=true" "-Dqcam=disabled" "-Dlc-compliance=disabled" ]; + patches = (old.patches or [ ]) ++ [ ./libcamera.patch ]; + }); + + libcamera-apps = final.callPackage ./libcamera-apps.nix { }; + + # newer version of rpi firmware + raspberrypifw = prev.raspberrypifw.overrideAttrs (old: { + src = prev.fetchFromGitHub { + owner = "raspberrypi"; + repo = "firmware"; + rev = "2cf8a179b3f2e6e5e5ceba4e8e544def10a49020"; + sha256 = "YG1bryflbV3W62MhZ/XMSgUJXMhCl/fe86x+CT7XZ4U="; + }; + }); + + # provide generic rpi arm64 u-boot + uboot_rpi_arm64 = prev.buildUBoot rec { + defconfig = "rpi_arm64_defconfig"; + extraMeta.platforms = [ "aarch64-linux" ]; + filesToInstall = [ "u-boot.bin" ]; + version = "2022.04"; + src = prev.fetchurl { + url = "ftp://ftp.denx.de/pub/u-boot/u-boot-${version}.tar.bz2"; + sha256 = "1l5w13dznj0z1ibqv2d6ljx2ma1gnf5x5ay3dqkqwxr6750nbq38"; + }; + }; + + # use a newer version of the rpi linux kernel fork + linux_rpi = prev.linux_rpi4.override { + argsOverride = rec { + src = prev.fetchFromGitHub { + owner = "raspberrypi"; + repo = "linux"; + rev = "9af1cc301e4dffb830025207a54d0bc63bec16c7"; + sha256 = "fsMTUdz1XZhPaSXpU1uBV4V4VxoZKi6cwP0QJcrCy1o="; + fetchSubmodules = true; + }; + version = "5.15.36"; + modDirVersion = "5.15.36"; + }; + }; +} diff --git a/overlay/libcamera-apps.nix b/overlay/libcamera-apps.nix new file mode 100644 index 0000000..82e04f9 --- /dev/null +++ b/overlay/libcamera-apps.nix @@ -0,0 +1,33 @@ +{ lib, stdenv, fetchFromGitHub, fetchpatch, cmake, pkg-config, libjpeg, libtiff +, libpng, libcamera, libepoxy, boost, libexif }: + +stdenv.mkDerivation rec { + pname = "libcamera-apps"; + version = "unstable-2022-05-12"; + + src = fetchFromGitHub { + owner = "raspberrypi"; + repo = "libcamera-apps"; + rev = "f5a2f1d86b440ebc064d4369421348d858ef31f3"; + sha256 = "Et8enICYct/AvWstY/id6BD/NB9+La9pNrtAsdwv+Tg="; + fetchSubmodules = true; + }; + + nativeBuildInputs = [ cmake pkg-config ]; + buildInputs = [ libjpeg libtiff libcamera libepoxy boost libexif libpng ]; + cmakeFlags = [ + "-DENABLE_QT=0" + "-DENABLE_OPENCV=0" + "-DENABLE_TFLITE=0" + "-DENABLE_X11=1" + "-DENABLE_DRM=1" + (if (stdenv.hostPlatform.isAarch64) then "-DARM64=ON" else "-DARM64=OFF") + ]; + + meta = with lib; { + description = "Userland tools interfacing with Raspberry Pi cameras"; + homepage = "https://github.com/raspberrypi/libcamera-apps"; + license = licenses.bsd2; + platforms = [ "aarch64-linux" ]; + }; +} diff --git a/overlay/libcamera.patch b/overlay/libcamera.patch new file mode 100644 index 0000000..8d7f9af --- /dev/null +++ b/overlay/libcamera.patch @@ -0,0 +1,19 @@ +diff --git a/src/libcamera/source_paths.cpp b/src/libcamera/source_paths.cpp +index 19689585..1380dfae 100644 +--- a/src/libcamera/source_paths.cpp ++++ b/src/libcamera/source_paths.cpp +@@ -39,14 +39,6 @@ namespace { + */ + bool isLibcameraInstalled() + { +- /* +- * DT_RUNPATH (DT_RPATH when the linker uses old dtags) is removed on +- * install. +- */ +- for (const ElfW(Dyn) *dyn = _DYNAMIC; dyn->d_tag != DT_NULL; ++dyn) { +- if (dyn->d_tag == DT_RUNPATH || dyn->d_tag == DT_RPATH) +- return false; +- } + + return true; + } \ No newline at end of file diff --git a/rpi-3b-plus/default.nix b/rpi-3b-plus/default.nix new file mode 100644 index 0000000..438aade --- /dev/null +++ b/rpi-3b-plus/default.nix @@ -0,0 +1,15 @@ +rpi: +{ lib, pkgs, config, ... }: + +{ + imports = [ rpi ]; + hardware.raspberry-pi.deviceTree = { + base-dtb = "bcm2710-rpi-3-b-plus.dtb"; + # u-boot expects bcm2837-rpi-3-b-plus.dtb for the 3b+ (as of + # 2020.04), although the kernel has 2710. We rename it to satisfy + # u-boot for now. + postInstall = '' + mv $out/broadcom/bcm2710-rpi-3-b-plus.dtb $out/broadcom/bcm2837-rpi-3-b-plus.dtb + ''; + }; +} diff --git a/rpi-4b/default.nix b/rpi-4b/default.nix new file mode 100644 index 0000000..c9141e2 --- /dev/null +++ b/rpi-4b/default.nix @@ -0,0 +1,8 @@ +rpi: +{ lib, pkgs, config, ... }: + +{ + imports = [ rpi ]; + hardware.raspberry-pi.deviceTree.base-dtb = "bcm2711-rpi-4-b.dtb"; +} + diff --git a/rpi-zero-2-w/default.nix b/rpi-zero-2-w/default.nix new file mode 100644 index 0000000..ac2d958 --- /dev/null +++ b/rpi-zero-2-w/default.nix @@ -0,0 +1,13 @@ +rpi: +{ lib, pkgs, config, ... }: + +{ + imports = [ rpi ]; + hardware.raspberry-pi.deviceTree.base-dtb = "bcm2710-rpi-zero-2.dtb"; + # u-boot expects bcm2837-rpi-zero-2.dtb for the zero 2 w (as of + # 2020.04), although the kernel has 2710. We rename it to satisfy + # u-boot for now. + hardware.raspberry-pi.deviceTree.postInstall = '' + mv $out/broadcom/bcm2710-rpi-zero-2.dtb $out/broadcom/bcm2837-rpi-zero-2.dtb + ''; +} diff --git a/rpi/audio.nix b/rpi/audio.nix new file mode 100644 index 0000000..1d2f79b --- /dev/null +++ b/rpi/audio.nix @@ -0,0 +1,17 @@ +{ config, lib, pkgs, ... }: + +let cfg = config.hardware.raspberry-pi.audio; +in { + options.hardware.raspberry-pi.audio = { + enable = lib.mkEnableOption "configuration for audio"; + }; + config = lib.mkIf cfg.enable { + hardware = { + raspberry-pi.deviceTree.base-dtb-params = [ "audio=on" ]; + pulseaudio.configFile = lib.mkOverride 990 + (pkgs.runCommand "default.pa" { } '' + sed 's/module-udev-detect$/module-udev-detect tsched=0/' ${config.hardware.pulseaudio.package}/etc/pulse/default.pa > $out + ''); + }; + }; +} diff --git a/rpi/default.nix b/rpi/default.nix new file mode 100644 index 0000000..26e6750 --- /dev/null +++ b/rpi/default.nix @@ -0,0 +1,26 @@ +{ overlay, nixpkgs }: +{ lib, pkgs, config, ... }: + +{ + imports = [ + (import ../sd-image nixpkgs) + ./device-tree.nix + ./audio.nix + ./i2c.nix + ./i2s.nix + ./modesetting.nix + ]; + + nixpkgs = { overlays = [ overlay ]; }; + boot = { + kernelPackages = pkgs.linuxPackagesFor (pkgs.linux_rpi); + initrd.availableKernelModules = [ "usbhid" "usb_storage" "vc4" ]; + + loader = { + grub.enable = lib.mkDefault false; + generic-extlinux-compatible.enable = lib.mkDefault true; + }; + }; + hardware.enableRedistributableFirmware = true; + +} diff --git a/rpi/device-tree.nix b/rpi/device-tree.nix new file mode 100644 index 0000000..bcf313f --- /dev/null +++ b/rpi/device-tree.nix @@ -0,0 +1,97 @@ +{ config, lib, pkgs, ... }: + +let cfg = config.hardware.raspberry-pi.deviceTree; +in { + options.hardware.raspberry-pi.deviceTree = { + base-dtb = lib.mkOption { + type = lib.types.str; + example = "bcm2711-rpi-4-b.dtb"; + description = "base dtb to apply"; + }; + base-dtb-params = lib.mkOption { + type = lib.types.listOf lib.types.string; + default = [ ]; + example = [ "i2c1=on" "audio=on" ]; + description = "parameters to pass to the base dtb"; + }; + dt-overlays = lib.mkOption { + type = with lib.types; + listOf (submodule { + options = { + overlay = lib.mkOption { type = str; }; + args = lib.mkOption { + type = listOf str; + default = [ ]; + }; + }; + }); + default = [ ]; + example = [{ + overlay = "vc4-fkms-v3d"; + args = [ "cma-512" ]; + }]; + description = "dtb overlays to apply"; + }; + postInstall = lib.mkOption { + type = lib.types.str; + default = ""; + description = "bash command to run after building dtb"; + }; + }; + config = { + hardware = { + deviceTree = { + enable = true; + filter = cfg.base-dtb; + package = let + dtbsWithSymbols = pkgs.stdenv.mkDerivation { + name = "dtbs-with-symbols"; + inherit (config.boot.kernelPackages.kernel) + src nativeBuildInputs depsBuildBuild; + patches = map (patch: patch.patch) + config.boot.kernelPackages.kernel.kernelPatches; + buildPhase = '' + patchShebangs scripts/* + substituteInPlace scripts/Makefile.lib \ + --replace 'DTC_FLAGS += $(DTC_FLAGS_$(basetarget))' 'DTC_FLAGS += $(DTC_FLAGS_$(basetarget)) -@' + make ${pkgs.stdenv.hostPlatform.linux-kernel.baseConfig} ARCH="${pkgs.stdenv.hostPlatform.linuxArch}" + make dtbs ARCH="${pkgs.stdenv.hostPlatform.linuxArch}" + ''; + installPhase = '' + make dtbs_install INSTALL_DTBS_PATH=$out/dtbs ARCH="${pkgs.stdenv.hostPlatform.linuxArch}" + ''; + }; + in lib.mkForce (pkgs.runCommand "device-tree-overlays" { + buildInputs = with pkgs; [ findutils libraspberrypi ]; + } '' + cd ${dtbsWithSymbols}/dtbs + for dtb in $(find . -type f -name "${config.hardware.deviceTree.filter}") + do + install -D $dtb $out/$dtb + + ${ + lib.concatMapStrings (param: '' + dtmerge -d $out/$dtb{,-merged} - ${param} + mv $out/$dtb{-merged,} + '') cfg.base-dtb-params + } + + ${ + lib.concatMapStrings (x: '' + dtmerge -d $out/$dtb{,-merged} ${x.overlay} ${ + builtins.concatStringsSep " " x.args + } + mv $out/$dtb{-merged,} + '') (map (x: + x // { + overlay = + "${config.boot.kernelPackages.kernel}/dtbs/overlays/${x.overlay}.dtbo"; + }) cfg.dt-overlays) + } + done + ${cfg.postInstall} + ''); + }; + }; + }; +} diff --git a/rpi/i2c.nix b/rpi/i2c.nix new file mode 100644 index 0000000..a59021f --- /dev/null +++ b/rpi/i2c.nix @@ -0,0 +1,14 @@ +{ config, lib, pkgs, ... }: + +let cfg = config.hardware.raspberry-pi.i2c; +in { + options.hardware.raspberry-pi.i2c = { + enable = lib.mkEnableOption "configuration for i2c"; + }; + config = lib.mkIf cfg.enable { + hardware = { + raspberry-pi.deviceTree.base-dtb-params = [ "i2c1=on" ]; + i2c.enable = true; + }; + }; +} diff --git a/rpi/i2s.nix b/rpi/i2s.nix new file mode 100644 index 0000000..fc88deb --- /dev/null +++ b/rpi/i2s.nix @@ -0,0 +1,13 @@ +{ config, lib, pkgs, ... }: + +let cfg = config.hardware.raspberry-pi.i2s; +in { + options.hardware.raspberry-pi.i2s = { + enable = lib.mkEnableOption "configuration for i2s"; + }; + config = lib.mkIf cfg.enable { + hardware = { + raspberry-pi.deviceTree.base-dtb-params = [ "i2s=on" ]; + }; + }; +} diff --git a/rpi/modesetting.nix b/rpi/modesetting.nix new file mode 100644 index 0000000..f967b58 --- /dev/null +++ b/rpi/modesetting.nix @@ -0,0 +1,23 @@ +{ config, lib, pkgs, ... }: + +let cfg = config.hardware.raspberry-pi.fkms-3d; +in { + options.hardware.raspberry-pi.fkms-3d = { + enable = lib.mkEnableOption "Enable modesetting through fkms-3d"; + }; + config = lib.mkIf cfg.enable { + hardware = { + raspberry-pi.deviceTree.dt-overlays = [ + { + overlay = "cma"; + args = [ ]; + } + { + overlay = "vc4-fkms-v3d"; + args = [ ]; + } + ]; + }; + services.xserver.videoDrivers = lib.mkBefore [ "modesetting" "fbdev" ]; + }; +} diff --git a/sd-image/default.nix b/sd-image/default.nix new file mode 100644 index 0000000..66b3a9e --- /dev/null +++ b/sd-image/default.nix @@ -0,0 +1,76 @@ +stable: +{ config, lib, pkgs, ... }: + +{ + imports = [ (import ./sd-image.nix stable) ]; + + boot.loader.grub.enable = false; + boot.loader.generic-extlinux-compatible.enable = true; + + boot.consoleLogLevel = lib.mkDefault 7; + + # The serial ports listed here are: + # - ttyS0: for Tegra (Jetson TX1) + # - ttyAMA0: for QEMU's -machine virt + boot.kernelParams = + [ "console=ttyS0,115200n8" "console=ttyAMA0,115200n8" "console=tty0" ]; + + sdImage = { + populateFirmwareCommands = let + configTxt = pkgs.writeText "config.txt" '' + [pi02] + kernel=u-boot-rpi_arm64.bin + + [pi3+] + kernel=u-boot-rpi_arm64.bin + + [pi4] + kernel=u-boot-rpi4.bin + enable_gic=1 + armstub=armstub8-gic.bin + arm_boost=1 + + # Otherwise the resolution will be weird in most cases, compared to + # what the pi3 firmware does by default. + disable_overscan=1 + + [all] + # Boot in 64-bit mode. + arm_64bit=1 + + # U-Boot needs this to work, regardless of whether UART is actually used or not. + # Look in arch/arm/mach-bcm283x/Kconfig in the U-Boot tree to see if this is still + # a requirement in the future. + enable_uart=1 + + # Prevent the firmware from smashing the framebuffer setup done by the mainline kernel + # when attempting to show low-voltage or overtemperature warnings. + avoid_warnings=1 + ''; + in '' + (cd ${pkgs.raspberrypifw}/share/raspberrypi/boot && cp bootcode.bin fixup*.dat start*.elf $NIX_BUILD_TOP/firmware/) + + # Add the config + cp ${configTxt} firmware/config.txt + + # Add rpi generic u-boot + cp ${pkgs.uboot_rpi_arm64}/u-boot.bin firmware/u-boot-rpi_arm64.bin + + # Add pi3 specific files + cp ${pkgs.raspberrypifw}/share/raspberrypi/boot/bcm2710-rpi-3-b-plus.dtb firmware/ + + # Add pi4 specific files + cp ${pkgs.ubootRaspberryPi4_64bit}/u-boot.bin firmware/u-boot-rpi4.bin + cp ${pkgs.raspberrypi-armstubs}/armstub8-gic.bin firmware/armstub8-gic.bin + cp ${pkgs.raspberrypifw}/share/raspberrypi/boot/bcm2711-rpi-4-b.dtb firmware/ + + # Add pi-zero-2 specific files + cp ${pkgs.raspberrypifw}/share/raspberrypi/boot/bcm2710-rpi-zero-2.dtb firmware/ + cp ${pkgs.raspberrypifw}/share/raspberrypi/boot/bcm2710-rpi-zero-2-w.dtb firmware/ + ''; + populateRootCommands = '' + mkdir -p ./files/boot + ${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot + ''; + }; +} diff --git a/sd-image/sd-image.nix b/sd-image/sd-image.nix new file mode 100644 index 0000000..c3891cc --- /dev/null +++ b/sd-image/sd-image.nix @@ -0,0 +1,281 @@ +nixpkgs: +# 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 + +{ config, lib, pkgs, ... }: + +with lib; + +let + rootfsImage = pkgs.callPackage "${nixpkgs}/nixos/lib/make-ext4-fs.nix" ({ + inherit (config.sdImage) storePaths; + compressImage = true; + 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. + ''; + }; + + firmwarePartitionName = mkOption { + type = types.str; + default = "FIRMWARE"; + description = '' + Name of the filesystem which holds the boot firmware. + ''; + }; + + 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 = 30; + 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 = true; + 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.sdImage.firmwarePartitionName}"; + fsType = "vfat"; + # Alternatively, this could be removed from the configuration. + # The filesystem is not needed at runtime, it could be treated + # as an opaque blob instead of a discrete FAT32 filesystem. + options = [ "nofail" "noauto" ]; + }; + "/" = { + 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" + zstd -d --no-progress "${rootfsImage}" -o ./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 <