├── .gitignore ├── blink.py ├── zero2w.nix ├── pi4.nix ├── sd-image.nix ├── README.md ├── flake.nix ├── flake.lock └── common.nix /.gitignore: -------------------------------------------------------------------------------- 1 | result 2 | -------------------------------------------------------------------------------- /blink.py: -------------------------------------------------------------------------------- 1 | import RPi.GPIO as GPIO # Import Raspberry Pi GPIO library 2 | from time import sleep # Import the sleep function from the time module 3 | GPIO.setwarnings(False) # Ignore warning for now 4 | GPIO.setmode(GPIO.BOARD) # Use physical pin numbering 5 | GPIO.setup(8, GPIO.OUT, initial=GPIO.LOW) # Set pin 8 to be an output pin and set initial value to low (off) 6 | while True: # Run forever 7 | GPIO.output(8, GPIO.HIGH) # Turn on 8 | sleep(1) # Sleep for 1 second 9 | GPIO.output(8, GPIO.LOW) # Turn off 10 | sleep(1) # Sleep for 1 second 11 | -------------------------------------------------------------------------------- /zero2w.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | modulesPath, 4 | pkgs, 5 | ... 6 | }: 7 | { 8 | imports = [ 9 | ./sd-image.nix 10 | ./common.nix 11 | ]; 12 | 13 | sdImage = { 14 | compressImage = false; 15 | imageName = "pi.img"; 16 | 17 | extraFirmwareConfig = { 18 | # Give up VRAM for more Free System Memory 19 | # - Disable camera which automatically reserves 128MB VRAM 20 | start_x = 0; 21 | 22 | # Reduce allocation of VRAM to 16MB minimum for non-rotated 23 | # (32MB for rotated) 24 | gpu_mem = 16; 25 | 26 | # Configure display to 800x600 so it fits on most screens 27 | # * See: https://elinux.org/RPi_Configuration 28 | hdmi_group = 2; 29 | hdmi_mode = 8; 30 | }; 31 | }; 32 | 33 | # this is handled by nixos-hardware on Pi 4 34 | boot = { 35 | initrd.availableKernelModules = [ 36 | "usbhid" 37 | "usb_storage" 38 | ]; 39 | }; 40 | 41 | networking.hostName = "nixos-zero2w"; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /pi4.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | modulesPath, 4 | pkgs, 5 | nixos-hardware, 6 | ... 7 | }: 8 | { 9 | imports = [ 10 | ./sd-image.nix 11 | ./common.nix 12 | "${nixos-hardware}/raspberry-pi/4" 13 | ]; 14 | 15 | sdImage = { 16 | compressImage = false; 17 | imageName = "pi.img"; 18 | 19 | extraFirmwareConfig = { 20 | # Give up VRAM for more Free System Memory 21 | # - Disable camera which automatically reserves 128MB VRAM 22 | start_x = 0; 23 | 24 | # Reduce allocation of VRAM to 16MB minimum for non-rotated 25 | # (32MB for rotated) 26 | gpu_mem = 16; 27 | 28 | # Configure display to 800x600 so it fits on most screens 29 | # * See: https://elinux.org/RPi_Configuration 30 | hdmi_group = 2; 31 | hdmi_mode = 8; 32 | }; 33 | }; 34 | 35 | hardware = { 36 | raspberry-pi."4" = { 37 | apply-overlays-dtmerge.enable = true; 38 | fkms-3d.enable = true; # rudolf 39 | }; 40 | deviceTree = { 41 | enable = true; 42 | }; 43 | }; 44 | 45 | networking.hostName = "nixos-pi4"; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /sd-image.nix: -------------------------------------------------------------------------------- 1 | # This module extends the official sd-image.nix with the following: 2 | # - ability to add options to the config.txt firmware 3 | { 4 | config, 5 | lib, 6 | pkgs, 7 | ... 8 | }: { 9 | options.sdImage = with lib; { 10 | extraFirmwareConfig = mkOption { 11 | type = types.attrs; 12 | default = {}; 13 | description = lib.mdDoc '' 14 | Extra configuration to be added to config.txt. 15 | ''; 16 | }; 17 | }; 18 | 19 | config = { 20 | sdImage.populateFirmwareCommands = 21 | lib.mkIf ((lib.length (lib.attrValues config.sdImage.extraFirmwareConfig)) > 0) 22 | ( 23 | let 24 | # Convert the set into a string of lines of "key=value" pairs. 25 | keyValueMap = name: value: name + "=" + toString value; 26 | keyValueList = lib.mapAttrsToList keyValueMap config.sdImage.extraFirmwareConfig; 27 | extraFirmwareConfigString = lib.concatStringsSep "\n" keyValueList; 28 | in 29 | lib.mkAfter 30 | '' 31 | config=firmware/config.txt 32 | # The initial file has just been created without write permissions. Add them to be able to append the file. 33 | chmod u+w $config 34 | echo "\n# Extra configuration" >> $config 35 | echo "${extraFirmwareConfigString}" >> $config 36 | chmod u-w $config 37 | '' 38 | ); 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building a NixOS SD image for a Raspberry Pi Zero 2 w / Pi 4 2 | 3 | 1. Update `common.nix` 4 | 5 | In particular, don't forget: 6 | - to configure your wifi 7 | - to change the admin user able to connect through ssh 8 | 9 | 2. Build the image 10 | 11 | Zero 2 W: 12 | 13 | ```sh 14 | nix build -L .#nixosConfigurations.zero2w.config.system.build.sdImage 15 | ``` 16 | 17 | Or for Pi 4: 18 | 19 | ```sh 20 | nix build -L .#nixosConfigurations.pi4.config.system.build.sdImage 21 | ``` 22 | 23 | 24 | 3. Copy the image in your sd card 25 | 26 | ```sh 27 | DEVICE=/dev/disk5 # Whatever your sd card reader is 28 | sudo dd if=result/sd-image/pi.img of=$DEVICE bs=1M conv=fsync status=progress 29 | ``` 30 | 31 | 4. Boot your Pi 32 | 5. Get your IP 33 | 34 | ```sh 35 | ifconfig wlan0 36 | ``` 37 | 38 | 6. From another machine, rebuild the system: 39 | 40 | ```sh 41 | ZERO2_IP= 42 | SSH_USER= 43 | ``` 44 | 45 | For Zero 2 W: 46 | 47 | ```sh 48 | nix run github:serokell/deploy-rs .#zero2w -- --ssh-user $SSH_USER --hostname $ZERO2_IP 49 | ``` 50 | 51 | For Pi 4: 52 | 53 | ```sh 54 | nix run github:serokell/deploy-rs .#pi4 -- --ssh-user $SSH_USER --hostname $ZERO2_IP 55 | ``` 56 | 57 | ## Notes 58 | 59 | - Various features are much better supported on the Pi 4 than on the Zero 2 W because the Pi 4 has a `nixos-hardware` profile. 60 | - The Zero 2 doesn't have enough RAM to build itself. An initial lead was to create a swap partition, but it turns out it was a bad idea, as it would have decreased the sd card lifetime (sd cards don't like many write operations). A `zram` swap is not big enough to work. Hence the use of `deploy-rs`. 61 | - Note that `nixos-rebuild --target-host` would work instead of using `deploy-rs`. but as `nixos-rebuild` is not available on Darwin, I'm using `deploy-rs` that works both on NixOS and Darwin. 62 | - I still couldn't find a way to use `boot.kernelPackages = pkgs.linuxKernel.packages.linux_rpi3`. 63 | - the `sdImage.extraFirmwareConfig` option is not ideal as it cannot update `config.txt` after it is created in the sd image. 64 | 65 | ## See also 66 | - [this issue](https://github.com/NixOS/nixpkgs/issues/216886) 67 | - [this gist](https://gist.github.com/plmercereau/0c8e6ed376dc77617a7231af319e3d29) 68 | 69 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Flake for building a Raspberry Pi Zero 2 SD image"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 6 | deploy-rs.url = "github:serokell/deploy-rs"; 7 | nixos-hardware.url = "github:NixOS/nixos-hardware"; 8 | }; 9 | 10 | outputs = { 11 | self, 12 | nixpkgs, 13 | deploy-rs, 14 | nixos-hardware 15 | }@inputs: 16 | let 17 | # see https://github.com/NixOS/nixpkgs/issues/154163 18 | overlays = [ 19 | (final: super: { 20 | makeModulesClosure = x: 21 | super.makeModulesClosure (x // { allowMissing = true; }); 22 | }) 23 | ]; 24 | specialArgs = { 25 | inherit nixos-hardware inputs; 26 | }; 27 | in rec { 28 | nixosConfigurations = { 29 | zero2w = nixpkgs.lib.nixosSystem { 30 | inherit specialArgs; 31 | modules = [ 32 | ({ config, pkgs, ... }: { nixpkgs.overlays = overlays; }) 33 | "${nixpkgs}/nixos/modules/installer/sd-card/sd-image-aarch64.nix" 34 | ./zero2w.nix 35 | ]; 36 | }; 37 | pi4 = nixpkgs.lib.nixosSystem { 38 | inherit specialArgs; 39 | modules = [ 40 | ({ config, pkgs, ... }: { nixpkgs.overlays = overlays; }) 41 | "${nixpkgs}/nixos/modules/installer/sd-card/sd-image-aarch64.nix" 42 | ./pi4.nix 43 | ]; 44 | }; 45 | }; 46 | 47 | deploy = { 48 | user = "root"; 49 | nodes = { 50 | zero2w = { 51 | hostname = "nix-zero2w"; 52 | profiles.system.path = 53 | deploy-rs.lib.aarch64-linux.activate.nixos self.nixosConfigurations.zero2w; 54 | #deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.zero2w; 55 | #remoteBuild = true; 56 | 57 | }; 58 | pi4 = { 59 | hostname = "nix-pi4"; 60 | profiles.system.path = 61 | deploy-rs.lib.aarch64-linux.activate.nixos self.nixosConfigurations.pi4; 62 | #deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.zero2w; 63 | #remoteBuild = true; 64 | 65 | }; 66 | }; 67 | }; 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "deploy-rs": { 4 | "inputs": { 5 | "flake-compat": "flake-compat", 6 | "nixpkgs": "nixpkgs", 7 | "utils": "utils" 8 | }, 9 | "locked": { 10 | "lastModified": 1708091384, 11 | "narHash": "sha256-dTGGw2y8wvfjr+J9CjQbfdulOq72hUG17HXVNxpH1yE=", 12 | "owner": "serokell", 13 | "repo": "deploy-rs", 14 | "rev": "0a0187794ac7f7a1e62cda3dabf8dc041f868790", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "serokell", 19 | "repo": "deploy-rs", 20 | "type": "github" 21 | } 22 | }, 23 | "flake-compat": { 24 | "flake": false, 25 | "locked": { 26 | "lastModified": 1696426674, 27 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 28 | "owner": "edolstra", 29 | "repo": "flake-compat", 30 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "edolstra", 35 | "repo": "flake-compat", 36 | "type": "github" 37 | } 38 | }, 39 | "nixos-hardware": { 40 | "locked": { 41 | "lastModified": 1709110790, 42 | "narHash": "sha256-qUk0G9vWX90beOKB1EtLFdeImXAujNi5SP5zTyIEATc=", 43 | "owner": "NixOS", 44 | "repo": "nixos-hardware", 45 | "rev": "01467901ec51dd92774040f2b3dff4f21f4e1c45", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "owner": "NixOS", 50 | "repo": "nixos-hardware", 51 | "type": "github" 52 | } 53 | }, 54 | "nixpkgs": { 55 | "locked": { 56 | "lastModified": 1702272962, 57 | "narHash": "sha256-D+zHwkwPc6oYQ4G3A1HuadopqRwUY/JkMwHz1YF7j4Q=", 58 | "owner": "NixOS", 59 | "repo": "nixpkgs", 60 | "rev": "e97b3e4186bcadf0ef1b6be22b8558eab1cdeb5d", 61 | "type": "github" 62 | }, 63 | "original": { 64 | "owner": "NixOS", 65 | "ref": "nixpkgs-unstable", 66 | "repo": "nixpkgs", 67 | "type": "github" 68 | } 69 | }, 70 | "nixpkgs_2": { 71 | "locked": { 72 | "lastModified": 1708847675, 73 | "narHash": "sha256-RUZ7KEs/a4EzRELYDGnRB6i7M1Izii3JD/LyzH0c6Tg=", 74 | "owner": "nixos", 75 | "repo": "nixpkgs", 76 | "rev": "2a34566b67bef34c551f204063faeecc444ae9da", 77 | "type": "github" 78 | }, 79 | "original": { 80 | "owner": "nixos", 81 | "ref": "nixpkgs-unstable", 82 | "repo": "nixpkgs", 83 | "type": "github" 84 | } 85 | }, 86 | "root": { 87 | "inputs": { 88 | "deploy-rs": "deploy-rs", 89 | "nixos-hardware": "nixos-hardware", 90 | "nixpkgs": "nixpkgs_2" 91 | } 92 | }, 93 | "systems": { 94 | "locked": { 95 | "lastModified": 1681028828, 96 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 97 | "owner": "nix-systems", 98 | "repo": "default", 99 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 100 | "type": "github" 101 | }, 102 | "original": { 103 | "owner": "nix-systems", 104 | "repo": "default", 105 | "type": "github" 106 | } 107 | }, 108 | "utils": { 109 | "inputs": { 110 | "systems": "systems" 111 | }, 112 | "locked": { 113 | "lastModified": 1701680307, 114 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", 115 | "owner": "numtide", 116 | "repo": "flake-utils", 117 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", 118 | "type": "github" 119 | }, 120 | "original": { 121 | "owner": "numtide", 122 | "repo": "flake-utils", 123 | "type": "github" 124 | } 125 | } 126 | }, 127 | "root": "root", 128 | "version": 7 129 | } 130 | -------------------------------------------------------------------------------- /common.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | pkgs, 4 | ... 5 | }: 6 | let 7 | python_with_packages = (pkgs.python311.withPackages (p: 8 | with p; [ 9 | pkgs.python311Packages.rpi-gpio 10 | pkgs.python311Packages.gpiozero 11 | pkgs.python311Packages.pyserial 12 | ])); 13 | in 14 | { 15 | nixpkgs.hostPlatform.system = "aarch64-linux"; 16 | nixpkgs.buildPlatform.system = "x86_64-linux"; 17 | # ! Need a trusted user for deploy-rs. 18 | nix.settings.trusted-users = ["@wheel"]; 19 | system.stateVersion = "24.05"; 20 | 21 | # don't build the NixOS docs locally 22 | documentation.nixos.enable = false; 23 | 24 | services.zram-generator = { 25 | enable = true; 26 | settings.zram0 = { 27 | compression-algorithm = "zstd"; 28 | zram-size = "ram * 2"; 29 | }; 30 | }; 31 | 32 | # Keep this to make sure wifi works 33 | hardware.enableRedistributableFirmware = lib.mkForce false; 34 | hardware.firmware = [pkgs.raspberrypiWirelessFirmware]; 35 | 36 | users.groups.gpio = {}; 37 | 38 | # services.udev.extraRules = '' 39 | # SUBSYSTEM=="bcm2835-gpiomem", KERNEL=="gpiomem", GROUP="gpio",MODE="0660" 40 | # SUBSYSTEM=="gpio", KERNEL=="gpiochip*", ACTION=="add", RUN+="${pkgs.bash}/bin/bash -c 'chown root:gpio /sys/class/gpio/export /sys/class/gpio/unexport ; chmod 220 /sys/class/gpio/export /sys/class/gpio/unexport'" 41 | # SUBSYSTEM=="gpio", KERNEL=="gpio*", ACTION=="add",RUN+="${pkgs.bash}/bin/bash -c 'chown root:gpio /sys%p/active_low /sys%p/direction /sys%p/edge /sys%p/value ; chmod 660 /sys%p/active_low /sys%p/direction /sys%p/edge /sys%p/value'" 42 | # ''; 43 | 44 | # https://raspberrypi.stackexchange.com/questions/40105/access-gpio-pins-without-root-no-access-to-dev-mem-try-running-as-root 45 | services.udev.extraRules = '' 46 | KERNEL=="gpiomem", GROUP="gpio", MODE="0660" 47 | SUBSYSTEM=="gpio", KERNEL=="gpiochip*", ACTION=="add", PROGRAM="${pkgs.bash}/bin/bash -c '${pkgs.coreutils}/bin/chgrp -R gpio /sys/class/gpio && ${pkgs.coreutils}/bin/chmod -R g=u /sys/class/gpio'" 48 | SUBSYSTEM=="gpio", ACTION=="add", PROGRAM="${pkgs.bash}/bin/bash -c '${pkgs.coreutils}/bin/chgrp -R gpio /sys%p && ${pkgs.coreutils}/bin/chmod -R g=u /sys%p'" 49 | ''; 50 | 51 | boot = { 52 | loader = { 53 | grub.enable = false; 54 | generic-extlinux-compatible.enable = true; 55 | timeout = 2; 56 | }; 57 | 58 | # https://artemis.sh/2023/06/06/cross-compile-nixos-for-great-good.html 59 | # for deploy-rs 60 | # binfmt.emulatedSystems = [ "x86_64-linux" ]; 61 | 62 | # Avoids warning: mdadm: Neither MAILADDR nor PROGRAM has been set. 63 | # This will cause the `mdmon` service to crash. 64 | # See: https://github.com/NixOS/nixpkgs/issues/254807 65 | swraid.enable = lib.mkForce false; 66 | }; 67 | 68 | sound.enable = true; 69 | 70 | hardware.pulseaudio.enable = true; 71 | 72 | # systemd.services.btattach = { 73 | # before = [ "bluetooth.service" ]; 74 | # after = [ "dev-ttyAMA0.device" ]; 75 | # wantedBy = [ "multi-user.target" ]; 76 | # serviceConfig = { 77 | # ExecStart = '' 78 | # ${pkgs.bluez}/bin/btattach -B /dev/ttyAMA0 -P bcm -S 3000000 79 | # ''; 80 | # }; 81 | # }; 82 | 83 | networking = { 84 | useDHCP = true; 85 | wireless = { 86 | enable = true; 87 | interfaces = ["wlan0"]; 88 | # ! Change the following to connect to your own network 89 | networks = { 90 | "ytvid-rpi" = { # SSID 91 | psk = "ytvid-rpi"; # password 92 | }; 93 | }; 94 | }; 95 | }; 96 | 97 | services.dnsmasq.enable = true; 98 | 99 | # Enable OpenSSH out of the box. 100 | services.sshd.enable = true; 101 | 102 | # NTP time sync. 103 | services.timesyncd.enable = true; 104 | 105 | # ! Change the following configuration 106 | users.users.chrism = { 107 | isNormalUser = true; 108 | home = "/home/chrism"; 109 | description = "Chris McDonough"; 110 | extraGroups = ["wheel" "networkmanager" "gpio" "audio"]; 111 | # ! Be sure to put your own public key here 112 | openssh = { 113 | authorizedKeys.keys = [ 114 | "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOLXUsGqUIEMfcXoIiiItmGNqOucJjx5D6ZEE3KgLKYV ednesia" 115 | ]; 116 | }; 117 | }; 118 | 119 | security.sudo = { 120 | enable = true; 121 | wheelNeedsPassword = false; 122 | }; 123 | 124 | # ! Be sure to change the autologinUser. 125 | services.getty.autologinUser = "chrism"; 126 | 127 | environment.systemPackages = with pkgs; [ 128 | libraspberrypi 129 | raspberrypi-eeprom 130 | htop 131 | vim 132 | emacs 133 | ripgrep 134 | btop 135 | python_with_packages 136 | usbutils 137 | tmux 138 | git 139 | lsof 140 | bat 141 | alsa-utils # aplay 142 | dig 143 | tree 144 | bintools 145 | file 146 | ethtool 147 | minicom 148 | bluez 149 | ]; 150 | 151 | 152 | } 153 | --------------------------------------------------------------------------------