diff --git a/deploy/nixos/build-iso.sh b/deploy/nixos/build-iso.sh index 9ad17f9..f449a55 100755 --- a/deploy/nixos/build-iso.sh +++ b/deploy/nixos/build-iso.sh @@ -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 # -# Usage: bash build-iso.sh +# Usage: bash build-iso.sh +# 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 " + 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 "" diff --git a/deploy/nixos/flake.nix b/deploy/nixos/flake.nix index 367b2dd..8ce95fb 100644 --- a/deploy/nixos/flake.nix +++ b/deploy/nixos/flake.nix @@ -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) diff --git a/deploy/nixos/flash-douro-usb.sh b/deploy/nixos/flash-douro-usb.sh new file mode 100755 index 0000000..aee581e --- /dev/null +++ b/deploy/nixos/flash-douro-usb.sh @@ -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 22" diff --git a/deploy/nixos/live.nix b/deploy/nixos/live.nix index 1f47608..d587860 100644 --- a/deploy/nixos/live.nix +++ b/deploy/nixos/live.nix @@ -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;