fix(deploy): multi-model ISO builds and HAL packaging

- Parameterize live.nix by machineModel (douro/tejo) passed via specialArgs
- Douro: kernel 5.15 LTS for Bay Trail i915 eDP fix, vt.handoff=7
- Fix HAL packaging: copy dist/ into subdirectory (not flattened)
- Add display-reset systemd service for kexec GPU reinitialization
- Add --disable-gpu flag for Electron on headless/GPU-less boots
- flake.nix: mkLiveConfig helper, per-model ISO outputs (iso-douro/iso-tejo)
- build-iso.sh: require model param, write model-specific .env for Vite
- flash-douro-usb.sh: GPT+FAT32 ESP layout for Bay Trail UEFI boot

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-25 17:02:28 -05:00
commit c874d9e2e3
4 changed files with 325 additions and 31 deletions

View file

@ -1,27 +1,92 @@
#!/usr/bin/env bash
# Build a bootable NixOS Live USB ISO for testing the ATM Electron app
# on physical hardware (UpBoard, Framework, etc).
# on physical hardware.
#
# After booting, provision credentials with: bash provision-atm.sh <atm-ip>
#
# Usage: bash build-iso.sh
# Usage: bash build-iso.sh <model>
# bash build-iso.sh douro # Bay Trail, kernel 5.15, GTQ
# bash build-iso.sh tejo # UP4000, kernel 6.6, USD
set -euo pipefail
MODEL="${1:-}"
if [ -z "$MODEL" ]; then
echo "Usage: bash build-iso.sh <model>"
echo " douro - Bay Trail Atom, kernel 5.15, eDP panel"
echo " tejo - UP4000/UPBoard, kernel 6.6"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
MACHINE_DIR="$REPO_ROOT/apps/machine"
ENV_FILE="$MACHINE_DIR/.env"
ENV_BACKUP=""
echo "=== Building Lamassu ATM Live USB ISO ==="
echo "=== Building Lamassu ATM Live USB ISO (model: $MODEL) ==="
# Step 1: Build the Electron app
# Step 1: Set machine-specific .env for Vite build (VITE_ vars are compile-time)
echo ""
echo "--- Step 1: Building Electron app ---"
echo "--- Step 1: Configuring .env for $MODEL ---"
if [ -f "$ENV_FILE" ]; then
ENV_BACKUP="$ENV_FILE.build-backup"
cp "$ENV_FILE" "$ENV_BACKUP"
echo "Backed up existing .env"
fi
# Write model-specific env (production endpoints baked into the build)
cat > "$ENV_FILE" << 'ENVEOF'
VITE_RELAY_URL=wss://relay.atm.aiolabs.dev
VITE_LIGHTNING_PUB_PUBKEY=64b0d9b1af689e99e4b8a23ee7b77715b394740a6830bdccf4718a060a53649a
VITE_LIGHTNING_PUB_API_URL=https://lp.atm.aiolabs.dev
VITE_ADMIN_TOKEN=lamassu-dev-admin-token
VITE_ATM_PRIVATE_KEY=f391a2c3fc734f443b0f685688a0441b5fb9805853c0023f570c5a3c6412b136
VITE_EXTENSION_API_URL=https://lp-ext.atm.aiolabs.dev
VITE_APP_ID=6016dadc6c677f131bc82cc0cb780d2b1090b83b8b7d0c972e469010270a4208
VITE_LNDCONNECT_URL=lndconnect://lnd.atm.aiolabs.dev:443?cert=&macaroon=AgEDbG5kAvgBAwoQjLYgcUni_8EKmwpX_vwOWxIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaIQoIbWFjYXJvb24SCGdlbmVyYXRlEgRyZWFkEgV3cml0ZRoWCgdtZXNzYWdlEgRyZWFkEgV3cml0ZRoXCghvZmZjaGFpbhIEcmVhZBIFd3JpdGUaFgoHb25jaGFpbhIEcmVhZBIFd3JpdGUaFAoFcGVlcnMSBHJlYWQSBXdyaXRlGhgKBnNpZ25lchIIZ2VuZXJhdGUSBHJlYWQAAAYgClsDux9_gPaKUK7PI54y-sTwt5WGmSzrzfKaKamwvK0
ENVEOF
# Append model-specific config
case "$MODEL" in
douro)
cat >> "$ENV_FILE" << 'EOF'
VITE_LAMASSU_MACHINE_MODEL=douro
VITE_LAMASSU_FIAT_CODE=GTQ
EOF
;;
tejo)
cat >> "$ENV_FILE" << 'EOF'
VITE_LAMASSU_MACHINE_MODEL=tejo
VITE_LAMASSU_FIAT_CODE=USD
EOF
;;
*)
echo "ERROR: Unknown model '$MODEL'. Use 'douro' or 'tejo'."
# Restore backup
if [ -n "$ENV_BACKUP" ]; then
mv "$ENV_BACKUP" "$ENV_FILE"
fi
exit 1
;;
esac
echo "Set VITE_LAMASSU_MACHINE_MODEL=$MODEL"
# Step 2: Build the Electron app (with model-specific .env baked in)
echo ""
echo "--- Step 2: Building Electron app ---"
cd "$REPO_ROOT"
pnpm run build --filter=@lamassu/machine
# Step 2: Verify build outputs exist
# Restore original .env
if [ -n "$ENV_BACKUP" ]; then
mv "$ENV_BACKUP" "$ENV_FILE"
echo "Restored original .env"
fi
# Step 3: Verify build outputs exist
echo ""
echo "--- Step 2: Verifying build outputs ---"
echo "--- Step 3: Verifying build outputs ---"
if [ ! -d "$MACHINE_DIR/dist" ]; then
echo "ERROR: $MACHINE_DIR/dist not found. Build failed?"
exit 1
@ -32,14 +97,14 @@ if [ ! -d "$MACHINE_DIR/dist-electron" ]; then
fi
echo "OK: dist/ and dist-electron/ present"
# Step 3: Build the NixOS ISO
# Step 4: Build the NixOS ISO
echo ""
echo "--- Step 3: Building NixOS ISO (this takes a while) ---"
echo "--- Step 4: Building NixOS ISO for $MODEL (this takes a while) ---"
cd "$SCRIPT_DIR"
export MACHINE_DIR="$MACHINE_DIR"
nix build .#iso --impure --show-trace
nix build ".#iso-${MODEL}" --impure --show-trace
# Step 4: Print results
# Step 5: Print results
ISO_PATH=$(ls result/iso/*.iso 2>/dev/null | head -1)
if [ -z "$ISO_PATH" ]; then
echo "ERROR: ISO not found in result/iso/"
@ -48,7 +113,7 @@ fi
ISO_SIZE=$(du -h "$ISO_PATH" | cut -f1)
echo ""
echo "=== ISO built successfully ==="
echo "=== ISO built successfully ($MODEL) ==="
echo "File: $ISO_PATH"
echo "Size: $ISO_SIZE"
echo ""

View file

@ -21,6 +21,13 @@
inherit system;
config.allowUnfree = true;
};
# Helper to create a live USB config for a specific machine model
mkLiveConfig = machineModel: nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = { inherit pkgs-unstable nixpkgs machineModel; };
modules = [ ./live.nix ];
};
in
{
# NixOS configuration for UP Board ATM (installed to disk)
@ -36,23 +43,23 @@
];
};
# Live USB configuration (boots from USB, no disk install)
nixosConfigurations.lamassu-live = nixpkgs.lib.nixosSystem {
inherit system;
# Live USB configurations per machine model
nixosConfigurations.lamassu-live-douro = mkLiveConfig "douro";
nixosConfigurations.lamassu-live-tejo = mkLiveConfig "tejo";
specialArgs = { inherit pkgs-unstable nixpkgs; };
modules = [
./live.nix
];
};
# Keep generic for backwards compat
nixosConfigurations.lamassu-live = mkLiveConfig "douro";
# Standalone module for importing into existing NixOS configs
nixosModules.default = import ./lamassu-atm.nix;
nixosModules.lamassu-atm = import ./lamassu-atm.nix;
packages.${system} = {
# ISO image for live USB testing
# ISO images per machine model
iso-douro = self.nixosConfigurations.lamassu-live-douro.config.system.build.isoImage;
iso-tejo = self.nixosConfigurations.lamassu-live-tejo.config.system.build.isoImage;
# Default (backwards compat)
iso = self.nixosConfigurations.lamassu-live.config.system.build.isoImage;
# SD card image (if needed)

195
deploy/nixos/flash-douro-usb.sh Executable file
View file

@ -0,0 +1,195 @@
#!/usr/bin/env bash
# Flash a NixOS Live ISO to a USB stick with a proper GPT + FAT32 ESP layout.
#
# Bay Trail (Douro) UEFI firmware can't boot from raw dd'd ISOs because it
# doesn't parse the El Torito EFI boot catalog embedded in ISO9660. This script
# creates a standard GPT partition with a FAT32 filesystem containing the
# EFI bootloader, kernel, initrd, and NixOS squashfs — which Bay Trail
# firmware recognizes natively.
#
# Usage: sudo bash flash-usb.sh /dev/sdX [path/to/iso]
#
# If no ISO path is given, uses the most recent build in result/iso/.
set -euo pipefail
USB_DEV="${1:-}"
ISO_PATH="${2:-}"
if [ -z "$USB_DEV" ]; then
echo "Usage: sudo bash flash-usb.sh /dev/sdX [path/to/iso]"
echo ""
echo " /dev/sdX USB device (NOT a partition — e.g. /dev/sdc, not /dev/sdc1)"
echo " path/to/iso Optional ISO path (default: result/iso/*.iso)"
echo ""
echo "WARNING: This will erase ALL data on the target device!"
exit 1
fi
# Safety checks
if [ "$(id -u)" -ne 0 ]; then
echo "ERROR: Must run as root (sudo)"
exit 1
fi
if [ ! -b "$USB_DEV" ]; then
echo "ERROR: $USB_DEV is not a block device"
exit 1
fi
# Don't accidentally wipe a hard drive
if echo "$USB_DEV" | grep -qE '^/dev/(sda|nvme|vda)'; then
echo "ERROR: $USB_DEV looks like a system drive. Refusing to proceed."
echo " Use a removable USB device (e.g. /dev/sdb, /dev/sdc)."
exit 1
fi
# Find ISO
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -z "$ISO_PATH" ]; then
ISO_PATH=$(ls "$SCRIPT_DIR"/result/iso/*.iso 2>/dev/null | head -1)
if [ -z "$ISO_PATH" ]; then
echo "ERROR: No ISO found in $SCRIPT_DIR/result/iso/"
echo " Build one first: bash build-iso.sh douro"
exit 1
fi
fi
if [ ! -f "$ISO_PATH" ]; then
echo "ERROR: ISO not found: $ISO_PATH"
exit 1
fi
ISO_SIZE=$(du -h "$ISO_PATH" | cut -f1)
USB_SIZE=$(lsblk -dno SIZE "$USB_DEV" 2>/dev/null || echo "unknown")
USB_MODEL=$(lsblk -dno MODEL "$USB_DEV" 2>/dev/null || echo "unknown")
echo "=== Flash NixOS Live USB ==="
echo "ISO: $ISO_PATH ($ISO_SIZE)"
echo "Target: $USB_DEV ($USB_SIZE, $USB_MODEL)"
echo ""
echo "WARNING: ALL data on $USB_DEV will be destroyed!"
read -p "Continue? (y/N) " -r
if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 0
fi
# Unmount any mounted partitions on the USB
echo ""
echo "--- Unmounting existing partitions ---"
for part in "${USB_DEV}"*; do
if mountpoint -q "$part" 2>/dev/null || mount | grep -q "^$part "; then
umount "$part" 2>/dev/null || true
echo "Unmounted $part"
fi
done
# Step 1: Create GPT partition table with a single FAT32 partition
echo ""
echo "--- Step 1: Creating GPT partition table ---"
parted -s "$USB_DEV" mklabel gpt
parted -s "$USB_DEV" mkpart ESP fat32 1MiB 100%
parted -s "$USB_DEV" set 1 esp on
echo "OK: GPT with ESP partition"
# Wait for kernel to pick up new partition table
sleep 2
partprobe "$USB_DEV" 2>/dev/null || true
sleep 1
# Find the partition device (handles both /dev/sdc1 and /dev/nvme0n1p1 styles)
PART_DEV="${USB_DEV}1"
if [ ! -b "$PART_DEV" ]; then
PART_DEV="${USB_DEV}p1"
fi
if [ ! -b "$PART_DEV" ]; then
echo "ERROR: Partition device not found (tried ${USB_DEV}1 and ${USB_DEV}p1)"
exit 1
fi
# Step 2: Format as FAT32
# FAT32 volume labels are max 11 chars, so we use a short label.
# The kernel finds root by LABEL=nixos-24.05-x86_64 which is on the ISO9660.
# But since we're extracting the ISO, we set the label on the FAT32 partition.
# NixOS initrd also searches by /nix-store.squashfs file presence.
echo ""
echo "--- Step 2: Formatting FAT32 ---"
mkfs.fat -F32 -n 'NIXOS2405' "$PART_DEV"
echo "OK: FAT32 formatted"
# Step 3: Mount ISO and USB partition
echo ""
echo "--- Step 3: Mounting ISO and USB ---"
MNT_ISO=$(mktemp -d)
MNT_USB=$(mktemp -d)
mount -o loop,ro "$ISO_PATH" "$MNT_ISO"
mount "$PART_DEV" "$MNT_USB"
echo "ISO mounted at $MNT_ISO"
echo "USB mounted at $MNT_USB"
# Cleanup function
cleanup() {
echo ""
echo "--- Cleaning up ---"
umount "$MNT_USB" 2>/dev/null || true
umount "$MNT_ISO" 2>/dev/null || true
rmdir "$MNT_ISO" 2>/dev/null || true
rmdir "$MNT_USB" 2>/dev/null || true
}
trap cleanup EXIT
# Step 4: Copy EFI boot files (UPPERCASE paths for Bay Trail UEFI compatibility)
echo ""
echo "--- Step 4: Copying EFI boot files ---"
mkdir -p "$MNT_USB/EFI/BOOT"
cp "$MNT_ISO/EFI/boot/bootx64.efi" "$MNT_USB/EFI/BOOT/BOOTX64.EFI"
cp "$MNT_ISO/EFI/boot/grub.cfg" "$MNT_USB/EFI/BOOT/grub.cfg"
cp -r "$MNT_ISO/EFI/boot/grub-theme" "$MNT_USB/EFI/BOOT/grub-theme"
cp "$MNT_ISO/EFI/boot/unicode.pf2" "$MNT_USB/EFI/BOOT/unicode.pf2"
cp "$MNT_ISO/EFI/boot/efi-background.png" "$MNT_USB/EFI/BOOT/efi-background.png"
if [ -f "$MNT_ISO/EFI/boot/refind_x64.efi" ]; then
cp "$MNT_ISO/EFI/boot/refind_x64.efi" "$MNT_USB/EFI/BOOT/refind_x64.efi"
fi
# Create the marker file GRUB searches for (search --file /EFI/nixos-installer-image)
mkdir -p "$MNT_USB/EFI"
touch "$MNT_USB/EFI/nixos-installer-image"
echo "OK: EFI boot files copied"
# Step 5: Copy boot directory (kernel, initrd, grub modules)
echo ""
echo "--- Step 5: Copying kernel and initrd ---"
cp -r "$MNT_ISO/boot" "$MNT_USB/boot"
echo "OK: Kernel and initrd copied"
# Step 6: Fix GRUB config paths (lowercase -> uppercase for our layout)
echo ""
echo "--- Step 6: Fixing GRUB config paths ---"
sed -i 's|/EFI/boot/|/EFI/BOOT/|g' "$MNT_USB/EFI/BOOT/grub.cfg"
echo "OK: GRUB paths updated"
# Step 7: Copy NixOS root filesystem (this is the big one ~2GB)
echo ""
echo "--- Step 7: Copying NixOS squashfs (this takes a while) ---"
cp "$MNT_ISO/nix-store.squashfs" "$MNT_USB/nix-store.squashfs"
if [ -f "$MNT_ISO/version.txt" ]; then
cp "$MNT_ISO/version.txt" "$MNT_USB/version.txt"
fi
echo "OK: squashfs copied"
# Step 8: Sync
echo ""
echo "--- Step 8: Syncing to disk ---"
sync
echo "OK: All data written"
# Cleanup happens via trap
echo ""
echo "=== USB flash complete ==="
echo "Device: $USB_DEV"
echo ""
echo "Now plug it into the target machine and boot from USB."
echo "The GRUB menu should appear with NixOS installer options."
echo ""
echo "After booting, provision credentials with:"
echo " bash provision-atm.sh <atm-ip> 22"

View file

@ -1,11 +1,16 @@
# Lamassu ATM Live USB Configuration
# Bootable ISO for testing on physical hardware (UpBoard) without installing to disk.
# Builds with: nix build .#iso
# Bootable ISO for testing on physical hardware without installing to disk.
#
# Parameterized by machineModel (passed via specialArgs from flake.nix):
# "douro" - Bay Trail Atom, kernel 5.15 (i915 regression in newer kernels), eDP panel
# "tejo" - UP4000/UPBoard, default kernel 6.6, standard Intel GPU
#
# Builds with: nix build .#iso-douro or nix build .#iso-tejo
#
# Does NOT import hardware/upboard.nix (its fileSystems conflict with live boot).
# Instead, duplicates only the hardware-relevant kernel modules and GPU config.
{ config, lib, pkgs, pkgs-unstable, nixpkgs, ... }:
{ config, lib, pkgs, pkgs-unstable, nixpkgs, machineModel ? "douro", ... }:
let
# Pre-built Electron app copied into the Nix store.
@ -47,8 +52,8 @@ let
cp -rL ${builtins.path { path = fileUriStore + "/file-uri-to-path"; name = "file-uri-to-path"; }} $out/node_modules/file-uri-to-path
# @lamassu/hal (workspace package, dynamically imported for hardware access)
mkdir -p $out/node_modules/@lamassu/hal
cp -rL ${builtins.path { path = halDir + "/dist"; name = "hal-dist"; }}/* $out/node_modules/@lamassu/hal/
mkdir -p $out/node_modules/@lamassu/hal/dist
cp -rL ${builtins.path { path = halDir + "/dist"; name = "hal-dist"; }}/* $out/node_modules/@lamassu/hal/dist/
cp ${builtins.path { path = halDir + "/package.json"; name = "hal-package-json"; }} $out/node_modules/@lamassu/hal/package.json
# serialport and transitive deps (native module chain for RS232 hardware)
@ -99,7 +104,7 @@ in
# ISO image settings
isoImage = {
isoName = "lamassu-atm-live.iso";
isoName = "lamassu-atm-${machineModel}-live.iso";
makeEfiBootable = true;
makeBiosBootable = true;
squashfsCompression = "zstd -Xcompression-level 6";
@ -108,7 +113,10 @@ in
# No fileSystems override needed — iso-image.nix handles squashfs + tmpfs root.
# We don't import hardware/upboard.nix, so there are no conflicting disk mounts.
# Boot: UpBoard-relevant kernel modules (from hardware/upboard.nix) without disk mounts
# Kernel: Douro Bay Trail needs 5.15 LTS (i915 eDP regression in 6.x kernels)
boot.kernelPackages = lib.mkIf (machineModel == "douro") pkgs.linuxPackages_5_15;
# Boot: kernel modules and parameters (model-specific)
boot = {
initrd.availableKernelModules = [
"xhci_pci"
@ -132,10 +140,13 @@ in
"i915.enable_psr=0"
"quiet"
"splash"
] ++ lib.optionals (machineModel == "douro") [
# Bay Trail: preserve BIOS display init (matches working kernel 5.4 config)
"vt.handoff=7"
];
};
# Intel GPU support (from hardware/upboard.nix)
# Intel GPU support
# NB: nixos-24.05 uses hardware.opengl, not hardware.graphics
hardware = {
opengl = {
@ -182,7 +193,7 @@ in
Environment = "LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib";
# Electron needs --no-sandbox in the live/testing environment
# --enable-logging makes renderer console.log visible in journalctl
ExecStart = lib.mkForce "${pkgs-unstable.electron}/bin/electron --no-sandbox --disable-gpu-sandbox --enable-logging ${atm-app}";
ExecStart = lib.mkForce "${pkgs-unstable.electron}/bin/electron --no-sandbox --disable-gpu-sandbox --disable-gpu --enable-logging ${atm-app}";
# Disable all security hardening that conflicts with Electron
NoNewPrivileges = lib.mkForce false;
ProtectSystem = lib.mkForce false;
@ -201,6 +212,22 @@ in
fi
'';
# Reset display output after X starts (required for kexec boots where
# the GPU wasn't reinitialized by BIOS firmware)
systemd.services.display-reset = {
description = "Reset eDP display output";
after = [ "display-manager.service" ];
requires = [ "display-manager.service" ];
wantedBy = [ "graphical.target" ];
before = [ "lamassu-atm.service" ];
serviceConfig = {
Type = "oneshot";
User = "lamassu";
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'";
};
};
# Allow SSH with password for initial setup on the live system
services.openssh.settings.PasswordAuthentication = lib.mkForce true;