-
-
Save plmercereau/0c8e6ed376dc77617a7231af319e3d29 to your computer and use it in GitHub Desktop.
| { config, lib, pkgs, ... }: | |
| { | |
| imports = [ | |
| <nixpkgs/nixos/modules/installer/sd-card/sd-image-aarch64-installer.nix> | |
| ./sd-image.nix | |
| ]; | |
| system.stateVersion = "23.11"; | |
| # Pi Zero 2 struggles to work without swap | |
| sdImage.swap.enable = true; | |
| sdImage.extraFirmwareConfig = { | |
| # Give up VRAM for more Free System Memory | |
| # - Disable camera which automatically reserves 128MB VRAM | |
| start_x = 0; | |
| # - Reduce allocation of VRAM to 16MB minimum for non-rotated (32MB for rotated) | |
| gpu_mem = 16; | |
| }; | |
| # bzip2 compression takes loads of time with emulation, skip it. Enable this if you're low on space. | |
| sdImage.compressImage = false; | |
| networking = { | |
| interfaces."wlan0".useDHCP = true; | |
| wireless = { | |
| enable = true; | |
| interfaces = [ "wlan0" ]; | |
| networks = { | |
| "<ssid>" = { | |
| psk = "<ssid-key>"; | |
| }; | |
| }; | |
| }; | |
| }; | |
| # Enable OpenSSH out of the box. | |
| services.sshd.enable = true; | |
| # NTP time sync. | |
| services.timesyncd.enable = true; | |
| } |
| # This module extends the official sd-image.nix with the following: | |
| # - ability to add a swap partition to the built image | |
| # - ability to add options to the config.txt firmware | |
| # - fix the uboot bug with pi zero 2 | |
| # Related issue: https://github.com/NixOS/nixpkgs/issues/216886 | |
| # Original file: https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/installer/sd-card/sd-image.nix | |
| { config, lib, pkgs, ... }: | |
| with lib; | |
| let | |
| rootfsImage = pkgs.callPackage <nixpkgs/nixos/lib/make-ext4-fs.nix> ({ | |
| inherit (config.sdImage) storePaths; | |
| compressImage = config.sdImage.compressImage; | |
| populateImageCommands = config.sdImage.populateRootCommands; | |
| volumeLabel = "NIXOS_SD"; | |
| } // optionalAttrs (config.sdImage.rootPartitionUUID != null) { | |
| uuid = config.sdImage.rootPartitionUUID; | |
| }); | |
| in | |
| { | |
| options.sdImage = { | |
| swap = { | |
| enable = mkEnableOption "Create a swap partition."; | |
| partitionName = mkOption { | |
| type = types.str; | |
| default = "SWAP"; | |
| description = lib.mdDoc '' | |
| Name of the partition which holds the swap. | |
| ''; | |
| }; | |
| size = mkOption { | |
| type = types.int; | |
| default = 2 * 1024; | |
| description = lib.mdDoc '' | |
| Size of the swap partition, in megabytes. | |
| ''; | |
| }; | |
| }; | |
| extraFirmwareConfig = mkOption { | |
| type = types.attrs; | |
| default = { }; | |
| description = lib.mdDoc '' | |
| Extra configuration to be added to config.txt. | |
| ''; | |
| }; | |
| }; | |
| config = { | |
| # Override of the sd image build to optionally add a swap partition | |
| system.build.sdImage = lib.mkForce (pkgs.callPackage | |
| ({ stdenv | |
| , dosfstools | |
| , e2fsprogs | |
| , mtools | |
| , libfaketime | |
| , util-linux | |
| , zstd | |
| }: stdenv.mkDerivation { | |
| name = config.sdImage.imageName; | |
| nativeBuildInputs = [ dosfstools e2fsprogs libfaketime mtools util-linux ] | |
| ++ lib.optional config.sdImage.compressImage zstd; | |
| inherit (config.sdImage) imageName 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 | |
| root_fs=${rootfsImage} | |
| ${lib.optionalString config.sdImage.compressImage '' | |
| root_fs=./root-fs.img | |
| echo "Decompressing rootfs image" | |
| zstd -d --no-progress "${rootfsImage}" -o $root_fs | |
| ''} | |
| # Set swap size. Set it to 0 it swap is disabled. | |
| swapSize=${toString (if config.sdImage.swap.enable then config.sdImage.swap.size else 0)} | |
| # The root partition is #2 if there is no swap, but is #3 is there is one | |
| rootPartitionNumber=${toString (if config.sdImage.swap.enable then 3 else 2)} | |
| # 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 | awk '{ print $1 }') | |
| firmwareSizeBlocks=$((${toString config.sdImage.firmwareSize} * 1024 * 1024 / 512)) | |
| # Note: swap size is 0 if swap is disabled | |
| imageSize=$((rootSizeBlocks * 512 + firmwareSizeBlocks * 512 + gap * 1024 * 1024 + swapSize * 1024 * 1024)) | |
| truncate -s $imageSize $img | |
| # type=b is 'W95 FAT32', type=82 is Swap, type=83 is 'Linux'. | |
| # The "bootable" partition is where u-boot will look file for the bootloader | |
| # information (dtbs, extlinux.conf file). | |
| sfdisk $img <<EOF | |
| label: dos | |
| label-id: ${config.sdImage.firmwarePartitionID} | |
| start=''${gap}M, size=$firmwareSizeBlocks, type=b | |
| ${lib.optionalString config.sdImage.swap.enable '' | |
| start=$((gap + ${toString config.sdImage.firmwareSize}))M, size=''${swapSize}M, type=82 | |
| ''} | |
| start=$((gap + ${toString config.sdImage.firmwareSize} + swapSize))M, type=83, bootable | |
| EOF | |
| # Copy the rootfs into the SD image | |
| eval $(partx $img -o START,SECTORS --nr $rootPartitionNumber --pairs) | |
| dd conv=notrunc if=$root_fs of=$img seek=$START count=$SECTORS | |
| # * Create the swap if it is enabled | |
| ${lib.optionalString config.sdImage.swap.enable '' | |
| # Create the swap | |
| eval $(partx $img -o START,SECTORS --nr 2 --pairs) | |
| dd if=/dev/zero of=swap.img bs=''${swapSize}M count=1 | |
| mkswap -L "${config.sdImage.swap.partitionName}" swap.img | |
| dd conv=notrunc if=swap.img of=$img seek=$START count=$SECTORS | |
| ''} | |
| # Create a FAT32 /boot/firmware partition of suitable size into firmware_part.img | |
| eval $(partx $img -o START,SECTORS --nr 1 --pairs) | |
| truncate -s $((SECTORS * 512)) firmware_part.img | |
| mkfs.vfat --invariant -i ${config.sdImage.firmwarePartitionID} -n ${config.sdImage.firmwarePartitionName} firmware_part.img | |
| # Populate the files intended for /boot/firmware | |
| mkdir firmware | |
| ${config.sdImage.populateFirmwareCommands} | |
| find firmware -exec touch --date=2000-01-01 {} + | |
| # Copy the populated /boot/firmware into the SD image | |
| cd firmware | |
| # Force a fixed order in mcopy for better determinism, and avoid file globbing | |
| for d in $(find . -type d -mindepth 1 | sort); do | |
| faketime "2000-01-01 00:00:00" mmd -i ../firmware_part.img "::/$d" | |
| done | |
| for f in $(find . -type f | sort); do | |
| mcopy -pvm -i ../firmware_part.img "$f" "::/$f" | |
| done | |
| cd .. | |
| # Verify the FAT partition before copying it. | |
| fsck.vfat -vn firmware_part.img | |
| dd conv=notrunc if=firmware_part.img of=$img seek=$START count=$SECTORS | |
| ${config.sdImage.postBuildCommands} | |
| if test -n "$compressImage"; then | |
| zstd -T$NIX_BUILD_CORES --rm $img | |
| fi | |
| ''; | |
| }) | |
| { }); | |
| swapDevices = lib.mkIf config.sdImage.swap.enable [{ | |
| device = "/dev/disk/by-label/${config.sdImage.swap.partitionName}"; | |
| }]; | |
| sdImage.populateFirmwareCommands = lib.mkIf ((lib.length (lib.attrValues config.sdImage.extraFirmwareConfig)) > 0) | |
| ( | |
| let | |
| # Convert the set into a string of lines of "key=value" pairs. | |
| keyValueMap = name: value: name + "=" + toString value; | |
| keyValueList = lib.mapAttrsToList keyValueMap config.sdImage.extraFirmwareConfig; | |
| extraFirmwareConfigString = lib.concatStringsSep "\n" keyValueList; | |
| in | |
| lib.mkAfter | |
| '' | |
| config=firmware/config.txt | |
| # The initial file has just been created without write permissions. Add them to be able to append the file. | |
| chmod u+w $config | |
| echo "\n# Extra configuration" >> $config | |
| echo "${extraFirmwareConfigString}" >> $config | |
| chmod u-w $config | |
| '' | |
| ); | |
| # Ugly hack to make it work with Pi Zero 2 | |
| sdImage.populateRootCommands = lib.mkForce '' | |
| mkdir -p ./files/boot | |
| ${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot | |
| DTBS_DIR=$(ls -d ./files/boot/nixos/*-dtbs)/broadcom | |
| chmod u+w $DTBS_DIR | |
| cp ${config.system.build.toplevel}/dtbs/broadcom/bcm2837-rpi-zero-2-w.dtb $DTBS_DIR/bcm2837-rpi-zero-2.dtb | |
| chmod u-w $DTBS_DIR | |
| ''; | |
| }; | |
| } |
Hi @plmercereau I just come across this when trying to figure out how to create a swap on my cross compiled image for raspberry pi, could you upsteam these changes to https://github.com/nixos/nixpkgs? Thanks!
@sullyj3 ever figure this out? I'm in the same boat
I did not!
I did not!
I've changed https://gist.github.com/plmercereau/0c8e6ed376dc77617a7231af319e3d29#file-sd-image-nix-L13 to
rootfsImage = pkgs.callPackage "${modulesPath}/../../nixos/lib/make-ext4-fs.nix" ({
You also need to make sure that modulesPath is added here https://gist.github.com/plmercereau/0c8e6ed376dc77617a7231af319e3d29#file-sd-image-nix-L8
Also, need to modify this line to also call modulesPath like so
https://gist.github.com/plmercereau/0c8e6ed376dc77617a7231af319e3d29#file-raspberry-pi-zero-2-nix-L4
"${modulesPath}/installer/sd-card/sd-image-aarch64-installer.nix"
Then it's just a matter of adding a new nixosConfiguration
nixosConfigurations = {
// nixos-stable is the stable nixos channel input in my flake
rpizero2wImage = nixos-stable.lib.nixosSystem {
system = "aarch64-linux";
// rpizero2w.nix is raspberry-pi-zero-2.nix from this gist
modules = [ ./modules/arm/rpizero2w.nix ];
};
};
Build using the following command. Drop -L if you don't want to see lots of logs scrolling on your screen.
nix build -L .#nixosConfigurations.rpizero2wImage.config.system.build.sdImage
That allowed me to build the image using flakes on my arm64 builder. I haven't tried booting it yet though, but at least it builds (:
Please see this flake example , and this comment
@plmercereau thanks so much! @jpraczyk I was so close! I figured it needed modulesPath but didn't have enough time to go the extra mile − many thanks for also working on it cc @sullyj3
Thanks very much!
For @sullyj3 @rjpcasalino and anyone else interested in related projects, I extended this project to implement several things:
- A custom kernel patch to enable USB host mode in the DTB
- A cross-compiled version to build on x86_64-linux systems
- Versions that work with both NixOS and Nix package manager on non-NixOS systems
- agenix encrypted secrets for the WiFi and admin user configuration
- Automation scripts to help build everything
You can check it out here:
https://github.com/pete3n/nix-pi/tree/main
Thanks for sharing this, Pete, that's a very inspiring repo. I wonder what and how we could submit upstream in order to simplify things a bit
this looks cool will def check it out when I've got time. Thanks @pete3n
Thanks, I hope you find it helpful. I have many projects involving different Raspberry Pis and other small board computers. As I get more proficient with Nix and build out my own library I will definitely look at submitting stuff upstream.
Nice! You both have a much better handle on nix lang than I do so I suggest looking into upstreaming stuff as soon as you can. https://github.com/samueldr seems to be a go to person for embedded stuff (check out https://github.com/Tow-Boot/Tow-Boot). I've started to wonder if there is a Raspberry Pi working group or some such. Anyway, please join https://discourse.nixos.org/ if you want. I'll share some of these links after I've played around with them so more. The community would love this stuff!
Thanks for this! Could I get some pointers on how to use it?
I want to add a raspberry pi zero 2 w image as an output of a flake, but I'm not entirely sure how to do so. If it makes a difference I'm on non-nixos, x86-64 linux.
My hazy guess of what to do, along with areas of uncertainty, is:
raspberry-pi-zero-2.nixnixos-rebuildornix build