Skip to content

Instantly share code, notes, and snippets.

@plmercereau
Last active January 10, 2024 13:46
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save plmercereau/0c8e6ed376dc77617a7231af319e3d29 to your computer and use it in GitHub Desktop.
Save plmercereau/0c8e6ed376dc77617a7231af319e3d29 to your computer and use it in GitHub Desktop.
Nix module to create SD images for Rasperry Pi Zero 2 W
{ 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
'';
};
}
@sullyj3
Copy link

sullyj3 commented Sep 16, 2023

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:

  1. Download the two files into the flake repo
  2. Create a new nixos configuration that imports raspberry-pi-zero-2.nix
  3. Set some options in that configuration
    • Do I need to set any options related to the sd-card stuff, or is that fully handled by the import?
  4. build it somehow.
    • Do I use nixos-rebuild or nix build
    • Does the nixos configuration itself become an sdcard image or will it be under a separate output?
    • Do I need to do something special to cross compile for ARM? If so how?

@izelnakri
Copy link

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!

@rjpcasalino
Copy link

rjpcasalino commented Nov 4, 2023

@sullyj3 ever figure this out? I'm in the same boat

@sullyj3
Copy link

sullyj3 commented Nov 5, 2023

I did not!

@jpraczyk
Copy link

jpraczyk commented Nov 6, 2023

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 (:

@plmercereau
Copy link
Author

plmercereau commented Nov 6, 2023

Please see this flake example , and this comment

@rjpcasalino
Copy link

rjpcasalino commented Nov 6, 2023

@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

@sullyj3
Copy link

sullyj3 commented Nov 7, 2023

Thanks very much!

@pete3n
Copy link

pete3n commented Dec 17, 2023

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

@plmercereau
Copy link
Author

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

@rjpcasalino
Copy link

this looks cool will def check it out when I've got time. Thanks @pete3n

@pete3n
Copy link

pete3n commented Dec 22, 2023

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.

@rjpcasalino
Copy link

rjpcasalino commented Dec 22, 2023

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment