Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.direnv/
result
result-*
26 changes: 26 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
MIXTAPE ?= coder
ARCH ?=
SSH_PORT ?= 2222
SSH_KEY ?=

define require-arch
$(if $(ARCH),,$(error ARCH is required. Use ARCH=aarch64-linux or ARCH=x86_64-linux))
Expand All @@ -36,6 +37,30 @@ build-kernel: ## Build kernel artifacts for kernel boot
$(call require-arch)
nix build .#packages.$(ARCH).$(MIXTAPE)-kernel-artifacts --impure

# -- Raspberry Pi image builds -----------------------------------------------
#
# RPi images are aarch64-only and ship as a single SD card image.
# The attr name adds "-rpi4-sd" / "-rpi5-sd" to the mixtape
# (e.g. coder → coder-rpi4-sd, coder → coder-rpi5-sd).

.PHONY: build-rpi4
build-rpi4: ## Build an SD card image for Raspberry Pi 4 (MIXTAPE=coder by default)
nix build .#packages.aarch64-linux.$(MIXTAPE)-rpi4-sd --impure -o result-rpi4

.PHONY: flash-rpi4
flash-rpi4: ## Flash RPi4 SD image to an SD card (SSH_KEY=~/.ssh/id_ed25519.pub optional)
@./scripts/flash-rpi.sh --board rpi4 \
$(if $(SSH_KEY),--ssh-key $(SSH_KEY))

.PHONY: build-rpi5
build-rpi5: ## Build an SD card image for Raspberry Pi 5 (MIXTAPE=coder by default)
nix build .#packages.aarch64-linux.$(MIXTAPE)-rpi5-sd --impure -o result-rpi5

.PHONY: flash-rpi5
flash-rpi5: ## Flash RPi5 SD image to an SD card (SSH_KEY=~/.ssh/id_ed25519.pub optional)
@./scripts/flash-rpi.sh --board rpi5 \
$(if $(SSH_KEY),--ssh-key $(SSH_KEY))

# -- VM development operations ------------------------------------------------

.PHONY: run
Expand Down Expand Up @@ -78,6 +103,7 @@ help: ## Show this help message
@echo " MIXTAPE=$(MIXTAPE)"
@echo " ARCH=$(ARCH)"
@echo " SSH_PORT=$(SSH_PORT)"
@echo " SSH_KEY=$(SSH_KEY)"

define print-target
@printf "Executing target: \033[36m$@\033[0m\n"
Expand Down
17 changes: 17 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";

# Raspberry Pi 4 / 5 configuration. The Pi 5 module sets
# boot.kernelPackages to the rpi-vendor 6.12.x kernel (built with
# bcm2712_defconfig and the RP1-southbridge initrd modules); without
# this input there is no Pi 5 kernel in nixpkgs at our pinned rev.
#
# Pinned to f1b7ff92cdd1 — the last commit before nixos-hardware#1841
# replaced the kernel's postConfigure sed with `LOCALVERSION = freeform ""`,
# which expands through nixpkgs' kernel-config emitter as the literal
# two characters `""`, breaking modDirVersion at build time. Tracked
# in nixos-hardware#1859; fix in #1860 is open but unmerged. Revisit
# the pin once #1860 (or any later fix) lands on master.
nixos-hardware.url = "github:NixOS/nixos-hardware/f1b7ff92cdd1";
dagger.url = "github:dagger/nix";
dagger.inputs.nixpkgs.follows = "nixpkgs";

Expand Down
54 changes: 53 additions & 1 deletion flake/images.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@
# packages.<system>.<mixtape-name>-kernel-artifacts (direct-kernel boot)
# packages.<system>.<mixtape-name>-dist (all formats + mixtape.toml)
#
# RPi mixtapes only emit an SD card image, suffixed by board:
# packages.aarch64-linux.<mixtape-name>-rpi4-sd (Pi 4 SD image)
# packages.aarch64-linux.<mixtape-name>-rpi5-sd (Pi 5 SD image)
#
# Build with:
# nix build .#packages.aarch64-linux.coder --impure
# nix build .#packages.x86_64-linux.coder --impure
# nix build .#packages.aarch64-linux.coder-rpi4-sd --impure
# nix build .#packages.aarch64-linux.coder-rpi5-sd --impure

{ self, inputs, ... }:

Expand All @@ -32,6 +38,37 @@ let
{ name = "coder-dev"; features = [ ../mixtapes/coder/package.nix ]; extraModules = [ ../profiles/dev.nix ]; }
];

# RPi 4 mixtape definitions — aarch64-only, SD image output.
# stereos.rpi.series defaults to "rpi4" so no inline override is needed.
rpi4MixtapeSpecs = [
{ name = "base-rpi4"; features = [ ../mixtapes/base/package.nix ]; extraModules = [ ../profiles/rpi.nix ]; }
{ name = "coder-rpi4"; features = [ ../mixtapes/coder/package.nix ]; extraModules = [ ../profiles/rpi.nix ]; }
{ name = "base-rpi4-dev"; features = [ ../mixtapes/base/package.nix ]; extraModules = [ ../profiles/rpi.nix ../profiles/dev.nix ]; }
{ name = "coder-rpi4-dev"; features = [ ../mixtapes/coder/package.nix ]; extraModules = [ ../profiles/rpi.nix ../profiles/dev.nix ]; }
];

# RPi 5 mixtape definitions — aarch64-only, SD image output.
# Each spec sets stereos.rpi.series = "rpi5" inline so modules/rpi.nix
# writes the Pi 5 firmware partition + [pi5] config.txt block, and pulls
# in nixos-hardware.raspberry-pi-5 for the rpi-vendor 6.12.x kernel
# (nixpkgs has no Pi 5 kernel package at our pinned rev).
rpi5MixtapeSpecs =
let
rpi5Modules = [
../profiles/rpi.nix
{ stereos.rpi.series = "rpi5"; }
inputs.nixos-hardware.nixosModules.raspberry-pi-5
# Skip modules that all-hardware.nix lists but the rpi-vendor
# kernel builds as =y. See modules/rpi5-kernel-overlay.nix.
../modules/rpi5-kernel-overlay.nix
];
in [
{ name = "base-rpi5"; features = [ ../mixtapes/base/package.nix ]; extraModules = rpi5Modules; }
{ name = "coder-rpi5"; features = [ ../mixtapes/coder/package.nix ]; extraModules = rpi5Modules; }
{ name = "base-rpi5-dev"; features = [ ../mixtapes/base/package.nix ]; extraModules = rpi5Modules ++ [ ../profiles/dev.nix ]; }
{ name = "coder-rpi5-dev"; features = [ ../mixtapes/coder/package.nix ]; extraModules = rpi5Modules ++ [ ../profiles/dev.nix ]; }
];

# Helper to build packages for a given system
buildSystemImages = system:
let
Expand Down Expand Up @@ -88,8 +125,23 @@ let
};
}) mixtapeNames
);
# RPi SD images — aarch64-linux only.
mkSdImagePkgs = specs:
if system == "aarch64-linux" then
builtins.listToAttrs (
builtins.map (spec: {
name = "${spec.name}-sd";
value = (stereos-main.mkMixtape {
inherit system;
inherit (spec) name features extraModules;
}).config.system.build.sdImage;
}) specs
)
else {};
rpi4Pkgs = mkSdImagePkgs rpi4MixtapeSpecs;
rpi5Pkgs = mkSdImagePkgs rpi5MixtapeSpecs;
in
rawPkgs // qcow2Named // kernelArtifactsNamed // distPkgs;
rawPkgs // qcow2Named // kernelArtifactsNamed // distPkgs // rpi4Pkgs // rpi5Pkgs;

# Build packages for all target systems
allPackages = builtins.listToAttrs (
Expand Down
87 changes: 87 additions & 0 deletions formats/rpi-sd-image.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# formats/rpi-sd-image.nix
#
# SD card image format for Raspberry Pi.
# Produces system.build.sdImage — an MBR-partitioned raw image with a
# FAT32 /boot partition (U-Boot + kernel + DTB) and an ext4 root.
#
# Build with:
# nix build .#packages.aarch64-linux.<mixtape-name>-sd --impure
#
# Flash with:
# zstd -d stereos-<mixtape>-rpi4.img.zst -o stereos.img
# dd if=stereos.img of=/dev/<sdcard> bs=4M status=progress

{ config, lib, pkgs, modulesPath, ... }:

let
# User-facing template for the first-boot key drop-in. See
# modules/firstboot-keys.nix for the service that consumes it.
authorizedKeysTemplate = pkgs.writeText "ssh_authorized_keys.txt" ''
# stereOS authorized_keys (first-boot drop-in)
#
# Add one SSH public key per line, in the standard OpenSSH
# authorized_keys format, e.g.
#
# ssh-ed25519 AAAA... matt@laptop
# ssh-rsa AAAAB3... workstation
#
# On every boot, stereos-firstboot-keys.service reads this file and
# appends any new keys to /home/admin/.ssh/authorized_keys before
# sshd starts. Blank lines and "#" comments are ignored. Duplicate
# keys are skipped. Safe to leave empty.
#
# This file lives on the FAT32 "FIRMWARE" partition of the SD card
# and can be edited directly from macOS / Windows / Linux with the
# card plugged into any computer.
'';
in
{
imports = [
# Provides system.build.sdImage, U-Boot extlinux bootloader setup,
# Raspberry Pi firmware in /boot, and fileSystems entries for
# NIXOS_SD (root) and FIRMWARE (boot).
"${modulesPath}/installer/sd-card/sd-image-aarch64.nix"
];

# `sdImage.imageBaseName` was renamed to `image.baseName` in nixpkgs 25.05.
image.baseName = "stereos-${config.networking.hostName}";

# Uncompressed — matches raw-efi.nix convention so downstream tools
# (dd, vm runners) can consume the artifact directly.
sdImage.compressImage = false;

# Drop the template onto the FIRMWARE partition. populateFirmwareCommands
# is declared as types.lines in nixpkgs, so this concatenates with the
# commands sd-image-aarch64.nix already installs.
sdImage.populateFirmwareCommands = ''
cp ${authorizedKeysTemplate} firmware/ssh_authorized_keys.txt
'';

# nixpkgs sd-image.nix mounts /boot/firmware with `noauto` because the
# FAT partition only holds RPi bootloader blobs consumed by the GPU
# firmware — nothing on the running Linux system needs it. We use it as
# the drop-in surface for user-editable config (ssh_authorized_keys.txt,
# future knobs), so override the options to mount it at boot. `nofail`
# stays so a corrupt/missing FAT doesn't block boot.
fileSystems."/boot/firmware".options = lib.mkForce [ "nofail" ];

# Disable fsck on the FIRMWARE partition. systemd auto-generates a
# systemd-fsck@... unit for every fstab entry with a nonzero passno,
# and failures there cascade into "Dependency failed for /boot/firmware"
# on the mount unit. vfat's dirty-bit set by unclean shutdowns is a
# frequent source; the partition holds small config files we're willing
# to re-flash if it ever corrupts.
fileSystems."/boot/firmware".noCheck = true;

# The sd-image build populates ./files/boot/... into the ext4 root, so
# /boot exists on the installed system but /boot/firmware does NOT —
# and systemd won't mount onto a missing path (nofail then swallows the
# error). systemd-tmpfiles runs after local-fs.target, so tmpfiles rules
# can't create the mount point in time. Instead bake the empty directory
# into the root filesystem at image build time.
# populateRootCommands is types.lines, so this concatenates with the
# sd-image-aarch64.nix block that writes extlinux.conf.
sdImage.populateRootCommands = ''
mkdir -p ./files/boot/firmware
'';
}
Loading
Loading