The Pi 4 twin of the Pi 5 build: `rpi4-installed` (in-place rebuild target),
`rpi4-image`, and `packages.aarch64-linux.{sd-image-rpi4,atm-app-rpi4}`, all
through the board-keyed machinery of the previous commit. The shared runtime
is untouched; only the board pair is new.
deploy/nixos/hardware/raspberry-pi-4.nix mirrors raspberry-pi-5.nix line for
line except where the boards differ:
- KMS for the kiosk display is an opt-in on the Pi 4
(`hardware.raspberry-pi."4".fkms-3d`), which also injects the CMA + vc4
device-tree overlays; the Pi 5 gets it by default. Without it X falls back
to the framebuffer and Electron renders in software.
- fkms-3d sets videoDrivers itself, so the module doesn't.
Everything else — extlinux boot, console pinned to tty0 so the GPIO UART is
free for a validator, no-suspend, the ttyValidator{0,1,2} udev symlinks — is
identical by design.
Evaluation-verified only: rpi4-installed/rpi4-image instantiate, and against
rpi5 they differ solely in the expected places (bcm2711 device tree, the two
fkms overlays, the rpiVersion=4 kernel, no clk-rp1 in initrd, machine model
in the env seed). Not yet booted on hardware; the doc says so.
docs/raspberry-pi-setup.md covers both boards — build, flash, first boot +
provisioning via the spire seed, in-place updates, peripherals — since #87
shipped the Pi 5 without one. It replaces a never-committed Pi 4 sketch
(parked on wip/rpi4-sketch) whose flake wiring didn't evaluate and whose
provisioning section predated the pairing seed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013A6683cCHnQxFUosx1krY4
738 lines
35 KiB
Nix
738 lines
35 KiB
Nix
{
|
|
description = "Lamassu Next - Nostr-Native Lightning ATM";
|
|
|
|
inputs = {
|
|
# Stable NixOS for the ATM OS base
|
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
|
|
|
# Unstable for Electron, Node.js, pnpm (latest versions)
|
|
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
|
|
|
flake-utils.url = "github:numtide/flake-utils";
|
|
|
|
rust-overlay = {
|
|
url = "github:oxalica/rust-overlay";
|
|
inputs.nixpkgs.follows = "nixpkgs-unstable";
|
|
};
|
|
|
|
devenv = {
|
|
url = "github:cachix/devenv";
|
|
inputs.nixpkgs.follows = "nixpkgs-unstable";
|
|
};
|
|
|
|
# Determinate Nix — ensures douro uses same nix version as dev machines
|
|
# so cachix binary cache hits match (nix 2.33 hashes == nix 2.33 hashes).
|
|
# The bare `?3` semver pin (≥3.0.0) was resolving to 3.16.3, which
|
|
# ships a regressed nix-functional-tests:local-overlay-store /
|
|
# stale-file-handle that FAILs when the determinate-nix derivation
|
|
# has to be built from source (no binary cache hit) — this blocked
|
|
# disk-image-sintra builds locally. `?3.15` (semver ≥3.15.0) skips
|
|
# past the regression; flake.lock currently resolves it to 3.20.0,
|
|
# which builds and ships nix 2.34.6.
|
|
determinate.url = "https://flakehub.com/f/DeterminateSystems/determinate/3.15";
|
|
|
|
# ATM TUI — operator management tool
|
|
atm-tui = {
|
|
url = "git+ssh://forgejo@git.atitlan.io/aiolabs/atm-tui.git";
|
|
inputs.nixpkgs.follows = "nixpkgs-unstable";
|
|
};
|
|
|
|
# Raspberry Pi 5 (aarch64) hardware support for the DIY Pi build.
|
|
nixos-hardware.url = "github:NixOS/nixos-hardware";
|
|
};
|
|
|
|
outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils, rust-overlay, devenv, determinate, atm-tui, nixos-hardware }:
|
|
let
|
|
system = "x86_64-linux";
|
|
|
|
pkgs = import nixpkgs {
|
|
inherit system;
|
|
config.allowUnfree = true;
|
|
};
|
|
|
|
pkgs-unstable = import nixpkgs-unstable {
|
|
inherit system;
|
|
config.allowUnfree = true;
|
|
overlays = [ (import rust-overlay) ];
|
|
};
|
|
|
|
# Pure ATM app builder (no --impure needed)
|
|
mkAtmApp = import ./nix/mkAtmApp.nix {
|
|
inherit pkgs pkgs-unstable;
|
|
src = self;
|
|
};
|
|
|
|
# aarch64 (Raspberry Pi 5) toolchain — a parallel set of pkgs + app
|
|
# builder for the DIY Pi build. Kept fully separate from the x86 fleet
|
|
# path so nothing above changes.
|
|
pkgsAarch64 = import nixpkgs {
|
|
system = "aarch64-linux";
|
|
config.allowUnfree = true;
|
|
};
|
|
pkgsUnstableAarch64 = import nixpkgs-unstable {
|
|
system = "aarch64-linux";
|
|
config.allowUnfree = true;
|
|
overlays = [ (import rust-overlay) ];
|
|
};
|
|
mkAtmAppAarch64 = import ./nix/mkAtmApp.nix {
|
|
pkgs = pkgsAarch64;
|
|
pkgs-unstable = pkgsUnstableAarch64;
|
|
src = self;
|
|
};
|
|
|
|
# Fiat code per machine model
|
|
fiatCodeForModel = {
|
|
douro = "GTQ";
|
|
tejo = "GTQ";
|
|
sintra = "EUR";
|
|
batm3 = "USD";
|
|
};
|
|
|
|
lib = nixpkgs.lib;
|
|
|
|
# Helper to create a live USB NixOS config for a specific machine model
|
|
mkLiveConfig = machineModel:
|
|
let
|
|
atm-app = mkAtmApp {
|
|
model = machineModel;
|
|
fiatCode = fiatCodeForModel.${machineModel} or "USD";
|
|
};
|
|
in
|
|
nixpkgs.lib.nixosSystem {
|
|
inherit system;
|
|
specialArgs = {
|
|
inherit pkgs-unstable nixpkgs machineModel atm-app;
|
|
};
|
|
modules = [
|
|
./deploy/nixos/live.nix
|
|
determinate.nixosModules.default
|
|
{
|
|
environment.systemPackages = [
|
|
atm-tui.packages.${system}.default
|
|
(pkgs.writeShellScriptBin "fund-atm" ''
|
|
exec ${pkgs-unstable.nodejs}/bin/node ${atm-app}/dist-electron/fund-atm.bundle.cjs "$@"
|
|
'')
|
|
];
|
|
environment.variables.ATM_DB_PATH = "/var/lib/bitspire/state.db";
|
|
}
|
|
];
|
|
};
|
|
|
|
# Helper to create a disk-installed NixOS config for a specific machine model.
|
|
# Unlike live configs (squashfs + tmpfs), installed configs use ext4 root
|
|
# and support `nixos-rebuild switch` for in-place updates.
|
|
mkInstalledConfig = machineModel: hardwareModule:
|
|
let
|
|
atm-app = mkAtmApp {
|
|
model = machineModel;
|
|
fiatCode = fiatCodeForModel.${machineModel} or "USD";
|
|
};
|
|
fiatCode = fiatCodeForModel.${machineModel} or "USD";
|
|
in
|
|
nixpkgs.lib.nixosSystem {
|
|
inherit system;
|
|
specialArgs = {
|
|
inherit pkgs-unstable atm-app;
|
|
};
|
|
modules = [
|
|
hardwareModule
|
|
./deploy/nixos/configuration.nix
|
|
./deploy/nixos/bitspire-atm.nix
|
|
determinate.nixosModules.default
|
|
({ config, lib, pkgs, ... }: {
|
|
services.bitspire = {
|
|
enable = true;
|
|
appDir = "${atm-app}";
|
|
};
|
|
|
|
# Operator TUI and CLI tools
|
|
environment.systemPackages = [
|
|
atm-tui.packages.${system}.default
|
|
(pkgs.writeShellScriptBin "fund-atm" ''
|
|
exec ${pkgs-unstable.nodejs}/bin/node ${atm-app}/dist-electron/fund-atm.bundle.cjs "$@"
|
|
'')
|
|
];
|
|
environment.variables.ATM_DB_PATH = "/var/lib/bitspire/state.db";
|
|
|
|
# Electron sandbox needs unprivileged user namespaces
|
|
boot.kernel.sysctl."kernel.unprivileged_userns_clone" = 1;
|
|
|
|
# Passwordless sudo for remote nixos-rebuild switch
|
|
security.sudo.wheelNeedsPassword = false;
|
|
|
|
# Allow bitspire user to use nix commands + pull from aiolabs binary cache.
|
|
# max-jobs = 1: prefer substitution from the cache, but allow ONE
|
|
# local build slot for tiny activation-time stitch derivations
|
|
# (boot.json, system-units, X-Restart-Triggers, etc.) that are
|
|
# inherently per-machine and can never be pre-cached. Heavy
|
|
# nixpkgs compiles (rustc, kernel, electron) are still
|
|
# effectively cache-only — they're upstream-cached, so a cache
|
|
# miss on them stays vanishingly rare in practice.
|
|
# max-jobs = 0 was tried first and silently bricked nightly
|
|
# auto-upgrades for 6+ days on an uncacheable trivial derivation
|
|
# (systemd-boot's boot.json).
|
|
nix.settings = {
|
|
max-jobs = 1;
|
|
# Hard ceiling on any local build's wall-clock time. Activation-
|
|
# time stitch derivations (boot.json, system-units, etc.) finish
|
|
# in well under a second; anything that doesn't return in 60s
|
|
# is by definition a heavy compile that has no business running
|
|
# on ATM hardware (kernel, electron, rustc). Kill it fast so
|
|
# the upgrade fails loudly instead of silently wedging the box
|
|
# for an hour. Time-bounds the max-jobs=1 escape hatch.
|
|
timeout = 60;
|
|
trusted-users = [ "root" "bitspire" ];
|
|
substituters = [ "https://cache.nixos.org" "https://aiolabs.cachix.org" ];
|
|
trusted-public-keys = [
|
|
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
|
"aiolabs.cachix.org-1:PrAjsGU9PE77tFKP2+iO+mgR88c4xv3utM9JmpTblUQ="
|
|
];
|
|
};
|
|
|
|
# Auto-upgrade: pulls latest flake and runs nixos-rebuild switch.
|
|
# NOTE: bitSpire machines pull from the `aiolabs/bitspire` repo —
|
|
# the post-migration home of this code. This branch (dev) pins the
|
|
# upgrade source to ?ref=dev so any ATM flashed from `dev` stays on
|
|
# `dev`. Without the explicit ?ref=dev, nix would resolve the repo's
|
|
# default branch and could silently change a dev-deployed Sintra at
|
|
# 04:00. The legacy `aiolabs/lamassu-next` repo still feeds the
|
|
# not-yet-converted production ATMs (batm3, douro) from its own
|
|
# branches; it is retired once those machines migrate to bitspire.
|
|
# To update manually: sudo nixos-rebuild switch --flake git+ssh://forgejo@git.atitlan.io/aiolabs/bitspire.git?ref=dev#<model>-installed
|
|
system.autoUpgrade = {
|
|
enable = true;
|
|
flake = "git+ssh://forgejo@git.atitlan.io/aiolabs/bitspire.git?ref=dev#${machineModel}-installed";
|
|
flags = [ "--refresh" ];
|
|
dates = "04:00"; # daily at 4am
|
|
allowReboot = false;
|
|
};
|
|
|
|
# Minimal env template (aiolabs/bitspire#70 remnant hygiene).
|
|
# Seed ONLY image-baked, non-maskable values. Everything else the
|
|
# ATM needs comes from the pairing SEED (relay, lnbits_npub, bunker)
|
|
# or from LNbits over the transport (operator pubkey, fee config) —
|
|
# so we must NOT pre-seed those keys. A present-but-empty
|
|
# VITE_RELAY_URL / VITE_LNBITS_SERVER_PUBKEY / VITE_OPERATOR_PUBKEYS
|
|
# is a masking hazard: env WINS over the seed, and this activation
|
|
# only writes when .env is ABSENT, so any value written at first
|
|
# boot is frozen for the life of the disk. Leaving the keys out
|
|
# entirely lets the seed/transport be the sole source.
|
|
#
|
|
# VITE_RELAY_URL / VITE_LNBITS_SERVER_PUBKEY are emitted ONLY when
|
|
# the operator deliberately pins them via the Nix options (non-empty
|
|
# default ""), which is an explicit override that wins over the seed.
|
|
system.activationScripts.bitspire-env = ''
|
|
mkdir -p /var/lib/bitspire
|
|
if [ ! -f /var/lib/bitspire/.env ]; then
|
|
cp ${pkgs.writeText "bitspire-env-default" (''
|
|
VITE_LAMASSU_MACHINE_MODEL=${machineModel}
|
|
VITE_LAMASSU_FIAT_CODE=${fiatCode}
|
|
VITE_SPIRE_SEED=
|
|
ELECTRON_FORCE_PROD=1
|
|
DISPLAY=:0
|
|
'' + pkgs.lib.optionalString (config.services.bitspire.relayUrl != "") ''
|
|
VITE_RELAY_URL=${config.services.bitspire.relayUrl}
|
|
'' + pkgs.lib.optionalString (config.services.bitspire.lnbitsServerPubkey != "") ''
|
|
VITE_LNBITS_SERVER_PUBKEY=${config.services.bitspire.lnbitsServerPubkey}
|
|
'')} /var/lib/bitspire/.env
|
|
chmod 600 /var/lib/bitspire/.env
|
|
chown bitspire:bitspire /var/lib/bitspire/.env
|
|
fi
|
|
'';
|
|
|
|
# Override systemd service for Electron runtime
|
|
systemd.services.bitspire = {
|
|
serviceConfig = {
|
|
EnvironmentFile = lib.mkForce "/var/lib/bitspire/.env";
|
|
Environment = "LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib";
|
|
ExecStart = lib.mkForce "${pkgs-unstable.electron}/bin/electron --no-sandbox --disable-gpu-sandbox --disable-gpu --disable-software-rasterizer --enable-logging ${atm-app}";
|
|
MemoryMax = lib.mkForce "1G";
|
|
NoNewPrivileges = lib.mkForce false;
|
|
ProtectSystem = lib.mkForce false;
|
|
ProtectHome = lib.mkForce false;
|
|
PrivateTmp = lib.mkForce false;
|
|
DevicePolicy = lib.mkForce "auto";
|
|
DeviceAllow = lib.mkForce [ "char-* rw" ];
|
|
};
|
|
};
|
|
|
|
# Reset eDP display output after X starts
|
|
systemd.services.display-reset = {
|
|
description = "Reset eDP display output";
|
|
after = [ "display-manager.service" ];
|
|
requires = [ "display-manager.service" ];
|
|
wantedBy = [ "graphical.target" ];
|
|
before = [ "bitspire.service" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "bitspire";
|
|
Environment = "DISPLAY=:0";
|
|
ExecStart = "${pkgs.bash}/bin/bash -c '${pkgs.xorg.xrandr}/bin/xrandr --output eDP-1 --off; sleep 1; ${pkgs.xorg.xrandr}/bin/xrandr --output eDP-1 --auto'";
|
|
};
|
|
};
|
|
|
|
# Swap file — ATMs have ~2GB RAM; prevents hard-freeze under memory pressure
|
|
swapDevices = [{ device = "/var/swapfile"; size = 1024; }];
|
|
|
|
# Clean /tmp on boot to prevent stale build artifacts filling disk
|
|
boot.tmp.cleanOnBoot = true;
|
|
|
|
# SSH with password for initial provisioning
|
|
services.openssh.settings.PasswordAuthentication = lib.mkForce true;
|
|
})
|
|
];
|
|
};
|
|
|
|
# Raspberry Pi 5 (aarch64) DIY build. Two products from one shared runtime:
|
|
# - mkPiInstalled: the in-place rebuild target. `nixos-rebuild switch
|
|
# --flake .#rpi5-installed` (or the git+ssh remote form) targets this.
|
|
# Declares the flashed media's own root fs (NIXOS_SD / FIRMWARE) and
|
|
# NOTHING image-specific, so a switch on a running Pi never trips over
|
|
# the sd-image builder.
|
|
# - mkPiImage: the same runtime + the aarch64 sd-image module, whose
|
|
# system.build.sdImage is the flashable artifact. The module supplies
|
|
# its OWN NIXOS_SD/FIRMWARE fileSystems + u-boot firmware, so we must
|
|
# not re-declare the root fs here (double definition = eval conflict).
|
|
#
|
|
# The runtime mirrors mkInstalledConfig (bitspire service, env activation,
|
|
# electron override, cache substituters) on aarch64 + Pi hardware, but
|
|
# deliberately drops the x86 fleet machinery for first bring-up: no
|
|
# determinate, no atm-tui (add once it ships an aarch64 package). Any Pi
|
|
# build needs an aarch64 builder (native Pi / arm box / binfmt emulation) —
|
|
# the app closure won't build on x86.
|
|
mkPiRuntime = machineModel: {
|
|
atm-app = mkAtmAppAarch64 {
|
|
model = machineModel;
|
|
fiatCode = fiatCodeForModel.${machineModel} or "USD";
|
|
};
|
|
fiatCode = fiatCodeForModel.${machineModel} or "USD";
|
|
};
|
|
|
|
# Supported Pi boards, keyed by machine model. Each pairs the
|
|
# nixos-hardware board module (kernel, firmware, bootloader, device tree)
|
|
# with our own hardware glue (validator serial, kiosk display, no-suspend).
|
|
# Everything else in the Pi runtime is board-agnostic.
|
|
piBoards = {
|
|
rpi5 = {
|
|
hardware = nixos-hardware.nixosModules.raspberry-pi-5;
|
|
glue = ./deploy/nixos/hardware/raspberry-pi-5.nix;
|
|
};
|
|
rpi4 = {
|
|
hardware = nixos-hardware.nixosModules.raspberry-pi-4;
|
|
glue = ./deploy/nixos/hardware/raspberry-pi-4.nix;
|
|
};
|
|
};
|
|
|
|
# Shared module list (everything EXCEPT the root fs and the sd-image
|
|
# builder). atm-app/fiatCode are threaded in so both products share one
|
|
# evaluated app closure.
|
|
piBaseModules = { machineModel, atm-app, fiatCode }:
|
|
let board = piBoards.${machineModel}; in [
|
|
board.hardware
|
|
./deploy/nixos/configuration.nix
|
|
./deploy/nixos/bitspire-atm.nix
|
|
board.glue
|
|
({ config, lib, pkgs, pkgs-unstable, ... }: {
|
|
services.bitspire = {
|
|
enable = true;
|
|
appDir = "${atm-app}";
|
|
};
|
|
|
|
environment.systemPackages = [
|
|
(pkgs.writeShellScriptBin "fund-atm" ''
|
|
exec ${pkgs-unstable.nodejs}/bin/node ${atm-app}/dist-electron/fund-atm.bundle.cjs "$@"
|
|
'')
|
|
];
|
|
environment.variables.ATM_DB_PATH = "/var/lib/bitspire/state.db";
|
|
|
|
boot.kernel.sysctl."kernel.unprivileged_userns_clone" = 1;
|
|
security.sudo.wheelNeedsPassword = false;
|
|
|
|
# Pull from the aiolabs binary cache so a `nixos-rebuild switch`
|
|
# (local or the git+ssh remote form) substitutes the heavy aarch64
|
|
# closure instead of compiling on the Pi. Mirrors the x86 fleet's
|
|
# nix.settings, minus max-jobs/timeout — the Pi 5 can actually build
|
|
# locally if it must, so we don't want the 60s watchdog killing a
|
|
# legitimate first build.
|
|
nix.settings = {
|
|
trusted-users = [ "root" "bitspire" ];
|
|
substituters = [ "https://cache.nixos.org" "https://aiolabs.cachix.org" ];
|
|
trusted-public-keys = [
|
|
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
|
"aiolabs.cachix.org-1:PrAjsGU9PE77tFKP2+iO+mgR88c4xv3utM9JmpTblUQ="
|
|
];
|
|
};
|
|
|
|
# Same first-boot env seed as the x86 installed configs.
|
|
system.activationScripts.bitspire-env = ''
|
|
mkdir -p /var/lib/bitspire
|
|
if [ ! -f /var/lib/bitspire/.env ]; then
|
|
cp ${pkgs.writeText "bitspire-env-default" (''
|
|
VITE_LAMASSU_MACHINE_MODEL=${machineModel}
|
|
VITE_LAMASSU_FIAT_CODE=${fiatCode}
|
|
VITE_SPIRE_SEED=
|
|
ELECTRON_FORCE_PROD=1
|
|
DISPLAY=:0
|
|
'' + pkgs.lib.optionalString (config.services.bitspire.relayUrl != "") ''
|
|
VITE_RELAY_URL=${config.services.bitspire.relayUrl}
|
|
'' + pkgs.lib.optionalString (config.services.bitspire.lnbitsServerPubkey != "") ''
|
|
VITE_LNBITS_SERVER_PUBKEY=${config.services.bitspire.lnbitsServerPubkey}
|
|
'')} /var/lib/bitspire/.env
|
|
chmod 600 /var/lib/bitspire/.env
|
|
chown bitspire:bitspire /var/lib/bitspire/.env
|
|
fi
|
|
'';
|
|
|
|
# Electron runtime override (same flags as the x86 fleet, aarch64
|
|
# electron). No eDP display-reset here — that's UP-Board-specific;
|
|
# the Pi drives HDMI/DSI directly.
|
|
systemd.services.bitspire.serviceConfig = {
|
|
EnvironmentFile = lib.mkForce "/var/lib/bitspire/.env";
|
|
Environment = "LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib";
|
|
ExecStart = lib.mkForce "${pkgs-unstable.electron}/bin/electron --no-sandbox --disable-gpu-sandbox --disable-software-rasterizer --enable-logging ${atm-app}";
|
|
MemoryMax = lib.mkForce "2G";
|
|
NoNewPrivileges = lib.mkForce false;
|
|
ProtectSystem = lib.mkForce false;
|
|
ProtectHome = lib.mkForce false;
|
|
PrivateTmp = lib.mkForce false;
|
|
DevicePolicy = lib.mkForce "auto";
|
|
DeviceAllow = lib.mkForce [ "char-* rw" ];
|
|
};
|
|
|
|
swapDevices = [{ device = "/var/swapfile"; size = 2048; }];
|
|
boot.tmp.cleanOnBoot = true;
|
|
services.openssh.settings.PasswordAuthentication = lib.mkForce true;
|
|
})
|
|
];
|
|
|
|
# In-place rebuild target — declares the flashed media's own filesystems
|
|
# (the labels mkPiImage's sd-image module writes), no image builder.
|
|
mkPiInstalled = machineModel:
|
|
let rt = mkPiRuntime machineModel; in
|
|
nixpkgs.lib.nixosSystem {
|
|
system = "aarch64-linux";
|
|
specialArgs = {
|
|
pkgs-unstable = pkgsUnstableAarch64;
|
|
inherit (rt) atm-app;
|
|
};
|
|
modules = (piBaseModules { inherit machineModel; inherit (rt) atm-app fiatCode; }) ++ [
|
|
{
|
|
fileSystems."/" = {
|
|
device = "/dev/disk/by-label/NIXOS_SD";
|
|
fsType = "ext4";
|
|
};
|
|
fileSystems."/boot/firmware" = {
|
|
device = "/dev/disk/by-label/FIRMWARE";
|
|
fsType = "vfat";
|
|
options = [ "nofail" "noauto" ];
|
|
};
|
|
}
|
|
];
|
|
};
|
|
|
|
# Flashable SD/USB image — same runtime + the aarch64 sd-image builder,
|
|
# which brings its own NIXOS_SD/FIRMWARE fileSystems and the u-boot
|
|
# firmware. Its system.build.sdImage is exposed as
|
|
# packages.aarch64-linux.sd-image-rpi5.
|
|
mkPiImage = machineModel:
|
|
let rt = mkPiRuntime machineModel; in
|
|
nixpkgs.lib.nixosSystem {
|
|
system = "aarch64-linux";
|
|
specialArgs = {
|
|
pkgs-unstable = pkgsUnstableAarch64;
|
|
inherit (rt) atm-app;
|
|
};
|
|
modules = (piBaseModules { inherit machineModel; inherit (rt) atm-app fiatCode; }) ++ [
|
|
(nixpkgs + "/nixos/modules/installer/sd-card/sd-image-aarch64.nix")
|
|
];
|
|
};
|
|
in
|
|
{
|
|
# ── NixOS Configurations (top-level, not per-system) ──────────
|
|
|
|
nixosConfigurations = {
|
|
# Ergonomic names: `nixos-rebuild switch --flake .#douro`
|
|
douro = mkLiveConfig "douro";
|
|
tejo = mkLiveConfig "tejo";
|
|
sintra = mkLiveConfig "sintra";
|
|
batm3 = mkLiveConfig "batm3";
|
|
|
|
# Backwards-compat aliases. Renamed lamassu-live-* → bitSpire-live-*
|
|
# for the brand transition; both styles available until callers
|
|
# (CI / scripts / docs) catch up. Drop the lamassu-* names once
|
|
# nothing references them.
|
|
bitSpire-live-douro = mkLiveConfig "douro";
|
|
bitSpire-live-tejo = mkLiveConfig "tejo";
|
|
bitSpire-live-sintra = mkLiveConfig "sintra";
|
|
bitSpire-live = mkLiveConfig "douro";
|
|
lamassu-live-douro = mkLiveConfig "douro";
|
|
lamassu-live-tejo = mkLiveConfig "tejo";
|
|
lamassu-live-sintra = mkLiveConfig "sintra";
|
|
lamassu-live = mkLiveConfig "douro";
|
|
|
|
# Installed-to-disk configs (proper GPT + systemd-boot, supports nixos-rebuild)
|
|
douro-installed = mkInstalledConfig "douro" ./deploy/nixos/hardware/douro.nix;
|
|
tejo-installed = mkInstalledConfig "tejo" ./deploy/nixos/hardware/upboard.nix;
|
|
# Sintra shares Aaeon UP Board hardware with tejo (same validator
|
|
# at ttyJ5, dispenser at ttyJ7 layout) — reuse the same hw module.
|
|
sintra-installed = mkInstalledConfig "sintra" ./deploy/nixos/hardware/upboard.nix;
|
|
batm3-installed = mkInstalledConfig "batm3" ./deploy/nixos/hardware/batm3.nix;
|
|
|
|
# Raspberry Pi 5 (aarch64) DIY build — Apex 7600 / NV10 over USB-serial.
|
|
# rpi5-installed → in-place rebuild target:
|
|
# sudo nixos-rebuild switch --flake \
|
|
# "git+ssh://forgejo@git.atitlan.io/aiolabs/bitspire.git?ref=<branch>#rpi5-installed"
|
|
# rpi5-image → source of the flashable image
|
|
# (packages.aarch64-linux.sd-image-rpi5).
|
|
rpi5-installed = mkPiInstalled "rpi5";
|
|
rpi5-image = mkPiImage "rpi5";
|
|
|
|
# Raspberry Pi 4 (aarch64) — same runtime and products as rpi5, on the
|
|
# previous-generation board (see deploy/nixos/hardware/raspberry-pi-4.nix
|
|
# for what differs). 4 GB minimum for Electron; 8 GB comfortable.
|
|
rpi4-installed = mkPiInstalled "rpi4";
|
|
rpi4-image = mkPiImage "rpi4";
|
|
|
|
# USB-bootable variant of batm3-installed. This is the config the
|
|
# flashed USB stick actually runs — distinct fs labels so stage-1 can't
|
|
# latch the internal drive, nofail /boot, no growPartition, autoUpgrade
|
|
# off. Exposed as a named config (not just inline in the disk-image
|
|
# target) so its system closure can be built here and deployed in-place
|
|
# with `nix copy` + `switch-to-configuration` — updating the app on a
|
|
# running stick WITHOUT reflashing (preserves pairing + /var/lib state).
|
|
# disk-image-batm3-usb builds its filesystem image from this same config.
|
|
batm3-usb = self.nixosConfigurations.batm3-installed.extendModules {
|
|
modules = [
|
|
({ lib, ... }: {
|
|
fileSystems."/".device = lib.mkForce "/dev/disk/by-label/nixos-usb";
|
|
fileSystems."/boot".device = lib.mkForce "/dev/disk/by-label/ESP-USB";
|
|
# /boot must NOT be a hard boot dependency on the USB image. The
|
|
# firmware already loaded the bootloader before Linux; without
|
|
# nofail, a slow/late ESP-USB enumeration (BOT is slower than UAS)
|
|
# blows past systemd's 90s device-timeout into emergency mode with
|
|
# root locked — a dead end. nofail + short timeout lets the
|
|
# already-mounted root carry the boot; /boot mounts if/when it shows.
|
|
fileSystems."/boot".options = [ "nofail" "x-systemd.device-timeout=10s" ];
|
|
# NO growPartition/autoResize: sfdisk rewriting the partition table
|
|
# on first boot is the single most bus-stressing write, and flaky
|
|
# USB bridges drop off the bus mid-rewrite (sfdisk wedges in D-state
|
|
# and ESP-USB vanishes with the device). Persistent state is a few
|
|
# MB and the image ships ~2GB free. The internal-SATA disk-image-
|
|
# batm3 keeps growPartition (a real AHCI SSD won't drop the bus).
|
|
system.autoUpgrade.enable = lib.mkForce false;
|
|
})
|
|
];
|
|
};
|
|
};
|
|
|
|
# ── Standalone NixOS module ───────────────────────────────────
|
|
|
|
nixosModules.default = import ./deploy/nixos/bitspire-atm.nix;
|
|
nixosModules.bitspire = import ./deploy/nixos/bitspire-atm.nix;
|
|
|
|
# ── Packages (x86_64-linux only for ATM hardware) ─────────────
|
|
|
|
packages.${system} = {
|
|
# Pure ATM app derivations
|
|
atm-app-douro = mkAtmApp { model = "douro"; fiatCode = "GTQ"; };
|
|
atm-app-tejo = mkAtmApp { model = "tejo"; fiatCode = "GTQ"; };
|
|
atm-app-sintra = mkAtmApp { model = "sintra"; fiatCode = "EUR"; };
|
|
atm-app-batm3 = mkAtmApp { model = "batm3"; fiatCode = "USD"; };
|
|
|
|
# ISO images
|
|
iso-douro = self.nixosConfigurations.douro.config.system.build.isoImage;
|
|
iso-tejo = self.nixosConfigurations.tejo.config.system.build.isoImage;
|
|
iso-sintra = self.nixosConfigurations.sintra.config.system.build.isoImage;
|
|
iso-batm3 = self.nixosConfigurations.batm3.config.system.build.isoImage;
|
|
|
|
# Raw disk images (dd-able to mSATA/eMMC, proper GPT + ESP)
|
|
disk-image-douro = import (nixpkgs + "/nixos/lib/make-disk-image.nix") {
|
|
inherit pkgs lib;
|
|
config = self.nixosConfigurations.douro-installed.config;
|
|
format = "raw";
|
|
partitionTableType = "efi";
|
|
diskSize = "auto";
|
|
};
|
|
|
|
disk-image-sintra = import (nixpkgs + "/nixos/lib/make-disk-image.nix") {
|
|
inherit pkgs lib;
|
|
config = self.nixosConfigurations.sintra-installed.config;
|
|
format = "raw";
|
|
partitionTableType = "efi";
|
|
diskSize = "auto";
|
|
};
|
|
|
|
# BATM3 (OptiPlex 9030 AIO board-swap) installed image, dd-able to
|
|
# its SATA drive. Unlike the douro/sintra images, this one grows
|
|
# itself: growPartition expands the root partition to fill whatever
|
|
# drive it lands on (16GB today) at first boot and autoResize
|
|
# stretches the ext4 to match — no manual parted/resize2fs step
|
|
# after flashing, and all the drive's headroom is available to the
|
|
# nix store from day one (cf. #55). Image-only override: once
|
|
# grown, subsequent nixos-rebuilds against plain batm3-installed
|
|
# are unaffected.
|
|
disk-image-batm3 =
|
|
let
|
|
cfg = self.nixosConfigurations.batm3-installed.extendModules {
|
|
modules = [
|
|
{
|
|
boot.growPartition = true;
|
|
fileSystems."/".autoResize = true;
|
|
}
|
|
];
|
|
};
|
|
in
|
|
import (nixpkgs + "/nixos/lib/make-disk-image.nix") {
|
|
inherit pkgs lib;
|
|
config = cfg.config;
|
|
format = "raw";
|
|
partitionTableType = "efi";
|
|
diskSize = "auto";
|
|
};
|
|
|
|
# USB-bootable Sintra image with DISTINCT partition labels
|
|
# (nixos-usb / ESP-USB) so the stick can be booted on a Sintra whose
|
|
# eMMC already holds a nixos/ESP-labelled install without a by-label
|
|
# collision — stage-1 would otherwise race between the two roots and
|
|
# likely mount the eMMC. Auto-upgrade is disabled: this is a portable
|
|
# test / hand-off image, not a managed fleet member, and disabling it
|
|
# also removes the scheduled bootloader writes that could otherwise
|
|
# land on the eMMC's ESP.
|
|
disk-image-sintra-usb =
|
|
let
|
|
cfg = self.nixosConfigurations.sintra-installed.extendModules {
|
|
modules = [
|
|
({ lib, ... }: {
|
|
fileSystems."/".device = lib.mkForce "/dev/disk/by-label/nixos-usb";
|
|
fileSystems."/boot".device = lib.mkForce "/dev/disk/by-label/ESP-USB";
|
|
system.autoUpgrade.enable = lib.mkForce false;
|
|
|
|
# The Sintra's Aaeon firmware USB-boots in Legacy/BIOS mode — it
|
|
# boots the live ISO via its isolinux (BIOS) El Torito image, not
|
|
# the UEFI ESP. systemd-boot is UEFI-only, so a dd'd systemd-boot
|
|
# image isn't recognised as bootable. Switch THIS USB image to
|
|
# GRUB with BOTH BIOS (MBR + bios_grub partition, via the "hybrid"
|
|
# table below) and UEFI (removable /EFI/BOOT/BOOTX64.EFI) — mirroring
|
|
# the live ISO's dual boot — so it boots on Legacy and UEFI alike.
|
|
# Scoped to the USB image; the eMMC install keeps systemd-boot.
|
|
boot.loader.systemd-boot.enable = lib.mkForce false;
|
|
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
|
|
boot.loader.grub = {
|
|
enable = lib.mkForce true;
|
|
efiSupport = true;
|
|
efiInstallAsRemovable = true;
|
|
# make-disk-image's build VM exposes the image as /dev/vda;
|
|
# GRUB installs its BIOS stage to that disk's MBR.
|
|
devices = lib.mkForce [ "/dev/vda" ];
|
|
};
|
|
})
|
|
];
|
|
};
|
|
baseImage = import (nixpkgs + "/nixos/lib/make-disk-image.nix") {
|
|
inherit pkgs lib;
|
|
config = cfg.config;
|
|
format = "raw";
|
|
# hybrid = GPT + bios_grub partition + ESP → BIOS + UEFI bootable.
|
|
partitionTableType = "hybrid";
|
|
diskSize = "auto";
|
|
label = "nixos-usb"; # ext4 root label (make-disk-image -L)
|
|
};
|
|
in
|
|
pkgs.runCommand "nixos-disk-image-sintra-usb"
|
|
{ nativeBuildInputs = [ pkgs.parted pkgs.mtools ]; }
|
|
''
|
|
mkdir -p $out
|
|
cp --sparse=always ${baseImage}/nixos.img $out/nixos.img
|
|
chmod +w $out/nixos.img
|
|
# make-disk-image hardcodes the ESP FAT label to "ESP"; relabel the
|
|
# volume to ESP-USB so /boot (by-label/ESP-USB) doesn't collide with
|
|
# the eMMC's ESP. Volume label only — bootloader files are untouched,
|
|
# and UEFI loads /EFI/BOOT/BOOTX64.EFI regardless of the label.
|
|
espStart=$(parted -sm "$out/nixos.img" unit B print | awk -F: '$1==1 {gsub("B","",$2); print $2}')
|
|
echo "ESP partition starts at byte $espStart — relabelling to ESP-USB"
|
|
export MTOOLS_SKIP_CHECK=1
|
|
mlabel -i "$out/nixos.img@@$espStart" ::ESP-USB
|
|
printf 'verify ESP label: '; mlabel -i "$out/nixos.img@@$espStart" -s :: || true
|
|
'';
|
|
|
|
# USB-bootable BATM3 TEST image with DISTINCT partition labels
|
|
# (nixos-usb / ESP-USB). The plain disk-image-batm3 reuses the generic
|
|
# nixos/ESP labels, so a USB stick carrying it, booted on a batm3 whose
|
|
# internal SATA drive ALREADY holds a nixos/ESP-labelled install, makes
|
|
# stage-1's by-label/nixos resolve to the internal drive (larger fs,
|
|
# journal recovers) instead of the stick — the stage-2 init path baked
|
|
# into the USB's boot entry isn't on that root, so stage 1 aborts.
|
|
# Distinct labels make stage-1 pick the stick unambiguously WITHOUT
|
|
# touching the internal drive. Unlike disk-image-sintra-usb this keeps
|
|
# systemd-boot: the batm3 firmware UEFI-USB-boots fine via the ESP's
|
|
# /EFI/BOOT/BOOTX64.EFI removable fallback, so no GRUB/hybrid-table
|
|
# change is needed — only the label disambiguation here plus the
|
|
# usb_storage/uas initrd modules (in batm3.nix). Does NOT grow to fill
|
|
# the stick (see the growPartition note below — sfdisk on first boot
|
|
# wedges flaky USB bridges); auto-upgrade off (test image, not a managed
|
|
# fleet member — also stops scheduled bootloader writes landing on the
|
|
# internal drive's ESP).
|
|
disk-image-batm3-usb =
|
|
let
|
|
# Filesystem image of the batm3-usb config (defined in
|
|
# nixosConfigurations). Same config that in-place deploys target, so
|
|
# a reflash and a `switch-to-configuration` converge on one system.
|
|
baseImage = import (nixpkgs + "/nixos/lib/make-disk-image.nix") {
|
|
inherit pkgs lib;
|
|
config = self.nixosConfigurations.batm3-usb.config;
|
|
format = "raw";
|
|
partitionTableType = "efi";
|
|
diskSize = "auto";
|
|
label = "nixos-usb"; # ext4 root label (make-disk-image -L)
|
|
};
|
|
in
|
|
pkgs.runCommand "nixos-disk-image-batm3-usb"
|
|
{ nativeBuildInputs = [ pkgs.parted pkgs.mtools ]; }
|
|
''
|
|
mkdir -p $out
|
|
cp --sparse=always ${baseImage}/nixos.img $out/nixos.img
|
|
chmod +w $out/nixos.img
|
|
# make-disk-image hardcodes the ESP FAT label to "ESP"; relabel the
|
|
# volume to ESP-USB so /boot (by-label/ESP-USB) can't resolve to an
|
|
# internal drive's ESP. Volume label only — bootloader files are
|
|
# untouched, and UEFI loads /EFI/BOOT/BOOTX64.EFI regardless.
|
|
espStart=$(parted -sm "$out/nixos.img" unit B print | awk -F: '$1==1 {gsub("B","",$2); print $2}')
|
|
echo "ESP partition starts at byte $espStart — relabelling to ESP-USB"
|
|
export MTOOLS_SKIP_CHECK=1
|
|
mlabel -i "$out/nixos.img@@$espStart" ::ESP-USB
|
|
printf 'verify ESP label: '; mlabel -i "$out/nixos.img@@$espStart" -s :: || true
|
|
'';
|
|
|
|
# Backwards compat
|
|
iso = self.nixosConfigurations.douro.config.system.build.isoImage;
|
|
};
|
|
|
|
# ── Packages (aarch64-linux — Raspberry Pi builds) ────────────
|
|
# Flashable SD images for the Pi boards. Build on an aarch64 builder
|
|
# (native Pi / arm box / `boot.binfmt` emulation on this x86 host):
|
|
# nix build .#packages.aarch64-linux.sd-image-rpi5
|
|
# nix build .#packages.aarch64-linux.sd-image-rpi4
|
|
packages.aarch64-linux = {
|
|
sd-image-rpi5 = self.nixosConfigurations.rpi5-image.config.system.build.sdImage;
|
|
atm-app-rpi5 = mkAtmAppAarch64 { model = "rpi5"; fiatCode = "USD"; };
|
|
sd-image-rpi4 = self.nixosConfigurations.rpi4-image.config.system.build.sdImage;
|
|
atm-app-rpi4 = mkAtmAppAarch64 { model = "rpi4"; fiatCode = "USD"; };
|
|
};
|
|
}
|
|
//
|
|
# ── Dev shells (per-system via flake-utils) ───────────────────
|
|
flake-utils.lib.eachDefaultSystem (sys:
|
|
let
|
|
dev-pkgs = import nixpkgs-unstable {
|
|
system = sys;
|
|
overlays = [ (import rust-overlay) ];
|
|
};
|
|
in
|
|
{
|
|
devShells.default = devenv.lib.mkShell {
|
|
pkgs = dev-pkgs;
|
|
modules = [ ./devenv.nix ];
|
|
};
|
|
}
|
|
);
|
|
}
|