diff --git a/deploy/nixos/build-iso.sh b/deploy/nixos/build-iso.sh index 843db9a..4849dd9 100755 --- a/deploy/nixos/build-iso.sh +++ b/deploy/nixos/build-iso.sh @@ -2,6 +2,7 @@ # Build a bootable NixOS Live USB ISO for testing the ATM Electron app # on physical hardware. # +# The build is fully pure — no local pnpm or .env manipulation needed. # After booting, provision credentials with: bash provision-atm.sh # # Usage: bash build-iso.sh @@ -19,100 +20,26 @@ if [ -z "$MODEL" ]; then 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 (model: $MODEL) ===" - -# Step 1: Set machine-specific .env for Vite build (VITE_ vars are compile-time) -echo "" -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=GTQ -EOF - ;; - sintra) - cat >> "$ENV_FILE" << 'EOF' -VITE_LAMASSU_MACHINE_MODEL=sintra -VITE_LAMASSU_FIAT_CODE=EUR -EOF - ;; + douro|tejo|sintra) ;; *) echo "ERROR: Unknown model '$MODEL'. Use 'douro', 'tejo', or 'sintra'." - # Restore backup - if [ -n "$ENV_BACKUP" ]; then - mv "$ENV_BACKUP" "$ENV_FILE" - fi exit 1 ;; esac -echo "Set VITE_LAMASSU_MACHINE_MODEL=$MODEL" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -# Step 2: Build the Electron app (with model-specific .env baked in) +echo "=== Building Lamassu ATM Live USB ISO (model: $MODEL) ===" echo "" -echo "--- Step 2: Building Electron app ---" +echo "This is a pure Nix build — no local pnpm required." +echo "" + cd "$REPO_ROOT" -pnpm run build --filter=@lamassu/machine +nix build ".#iso-${MODEL}" --show-trace -# 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 3: Verifying build outputs ---" -if [ ! -d "$MACHINE_DIR/dist" ]; then - echo "ERROR: $MACHINE_DIR/dist not found. Build failed?" - exit 1 -fi -if [ ! -d "$MACHINE_DIR/dist-electron" ]; then - echo "ERROR: $MACHINE_DIR/dist-electron not found. Build failed?" - exit 1 -fi -echo "OK: dist/ and dist-electron/ present" - -# Step 4: Build the NixOS ISO -echo "" -echo "--- Step 4: Building NixOS ISO for $MODEL (this takes a while) ---" -cd "$SCRIPT_DIR" -export MACHINE_DIR="$MACHINE_DIR" -nix build ".#iso-${MODEL}" --impure --show-trace - -# Step 5: Print results +# 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/" diff --git a/deploy/nixos/flake.lock b/deploy/nixos/flake.lock deleted file mode 100644 index 2beea6f..0000000 --- a/deploy/nixos/flake.lock +++ /dev/null @@ -1,44 +0,0 @@ -{ - "nodes": { - "nixpkgs": { - "locked": { - "lastModified": 1735563628, - "narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-24.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-unstable": { - "locked": { - "lastModified": 1771177547, - "narHash": "sha256-trTtk3WTOHz7hSw89xIIvahkgoFJYQ0G43IlqprFoMA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "ac055f38c798b0d87695240c7b761b82fc7e5bc2", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "nixpkgs": "nixpkgs", - "nixpkgs-unstable": "nixpkgs-unstable" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/deploy/nixos/flake.nix b/deploy/nixos/flake.nix deleted file mode 100644 index 3cab0c1..0000000 --- a/deploy/nixos/flake.nix +++ /dev/null @@ -1,71 +0,0 @@ -{ - description = "Lamassu Next ATM - NixOS Deployment"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; - - # For Electron/Node.js packaging - nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - }; - - outputs = { self, nixpkgs, nixpkgs-unstable }: - let - system = "x86_64-linux"; - - pkgs = import nixpkgs { - inherit system; - config.allowUnfree = true; - }; - - pkgs-unstable = import nixpkgs-unstable { - 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) - nixosConfigurations.lamassu-atm = nixpkgs.lib.nixosSystem { - inherit system; - - specialArgs = { inherit pkgs-unstable; }; - - modules = [ - ./hardware/upboard.nix - ./configuration.nix - ./lamassu-atm.nix - ]; - }; - - # Live USB configurations per machine model - nixosConfigurations.lamassu-live-douro = mkLiveConfig "douro"; - nixosConfigurations.lamassu-live-tejo = mkLiveConfig "tejo"; - nixosConfigurations.lamassu-live-sintra = mkLiveConfig "sintra"; - - # 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 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; - iso-sintra = self.nixosConfigurations.lamassu-live-sintra.config.system.build.isoImage; - - # Default (backwards compat) - iso = self.nixosConfigurations.lamassu-live.config.system.build.isoImage; - - # SD card image (if needed) - sdcard = self.nixosConfigurations.lamassu-atm.config.system.build.sdImage; - }; - }; -} diff --git a/deploy/nixos/hardware/douro.nix b/deploy/nixos/hardware/douro.nix new file mode 100644 index 0000000..5c868a4 --- /dev/null +++ b/deploy/nixos/hardware/douro.nix @@ -0,0 +1,89 @@ +# Douro (Bay Trail) Hardware Configuration +# Bay Trail Atom SoC with eDP panel, mSATA internal storage. +# +# Kernel 5.15 LTS required: i915 eDP display regression in 6.x kernels +# causes blank screen on Bay Trail with embedded DisplayPort panels. + +{ config, lib, pkgs, ... }: + +{ + boot = { + loader = { + systemd-boot.enable = true; + efi.canTouchEfiVariables = true; + timeout = 3; + }; + + # Bay Trail needs 5.15 LTS (i915 eDP regression in 6.x) + kernelPackages = pkgs.linuxPackages_5_15; + + initrd.availableKernelModules = [ + "xhci_pci" + "ahci" + "usb_storage" + "sd_mod" + "sdhci_pci" + "i915" + ]; + + kernelModules = [ + "kvm-intel" + "i2c-dev" + "spi-dev" + ]; + + kernelParams = [ + "i915.enable_psr=0" + "vt.handoff=7" # Bay Trail: preserve BIOS display init + "quiet" + "splash" + ]; + }; + + # Disk layout: GPT with ESP (sda1) + ext4 root (sda2) + fileSystems."/" = { + device = "/dev/disk/by-label/nixos"; + fsType = "ext4"; + }; + + # make-disk-image.nix labels the ESP as "ESP" (not "boot") + fileSystems."/boot" = { + device = "/dev/disk/by-label/ESP"; + fsType = "vfat"; + }; + + # NixOS 24.05 uses hardware.opengl (not hardware.graphics) + hardware = { + opengl = { + enable = true; + extraPackages = with pkgs; [ + intel-media-driver + vaapiIntel + vaapiVdpau + libvdpau-va-gl + ]; + }; + enableRedistributableFirmware = true; + cpu.intel.updateMicrocode = true; + }; + + powerManagement = { + enable = true; + cpuFreqGovernor = "performance"; + }; + + # Disable suspend/hibernate for kiosk + systemd.targets = { + sleep.enable = false; + suspend.enable = false; + hibernate.enable = false; + hybrid-sleep.enable = false; + }; + + # Serial port access for bill validator/dispenser + services.udev.extraRules = lib.mkAfter '' + KERNEL=="ttyS[0-9]*", MODE="0666" + KERNEL=="ttyUSB[0-9]*", MODE="0666" + KERNEL=="ttyACM[0-9]*", MODE="0666" + ''; +} diff --git a/deploy/nixos/live.nix b/deploy/nixos/live.nix index bfbd4fe..7b9ab99 100644 --- a/deploy/nixos/live.nix +++ b/deploy/nixos/live.nix @@ -10,100 +10,27 @@ # 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, machineModel ? "douro", ... }: +{ config, lib, pkgs, pkgs-unstable, nixpkgs, machineModel ? "douro", atm-app, ... }: let - # Pre-built Electron app copied into the Nix store. - # Build first: pnpm run build --filter=@lamassu/machine - # Requires --impure: reads MACHINE_DIR env var to find build artifacts (dist/ is gitignored) - machineDir = builtins.getEnv "MACHINE_DIR"; - # Resolve pnpm virtual store paths for native modules - pnpmStore = machineDir + "/../../node_modules/.pnpm"; - betterSqliteStore = pnpmStore + "/better-sqlite3@11.10.0/node_modules"; - bindingsStore = pnpmStore + "/bindings@1.5.0/node_modules"; - fileUriStore = pnpmStore + "/file-uri-to-path@1.0.0/node_modules"; - - # HAL package (workspace dep, resolved via symlink in pnpm) - halDir = machineDir + "/../../packages/hal"; - # serialport and its transitive native deps (required by @lamassu/hal at runtime) - serialportStore = pnpmStore + "/serialport@12.0.0/node_modules"; - serialportStreamStore = pnpmStore + "/@serialport+stream@12.0.0/node_modules/@serialport"; - serialportBindingsCppStore = pnpmStore + "/@serialport+bindings-cpp@12.0.1/node_modules/@serialport"; - serialportBindingsIfaceStore = pnpmStore + "/@serialport+bindings-interface@1.2.2/node_modules/@serialport"; - serialportBindingMockStore = pnpmStore + "/@serialport+binding-mock@10.2.2/node_modules/@serialport"; - # serialport parser sub-packages (barrel import in serialport@12 eagerly requires all of these) - parserStore = suffix: pnpmStore + "/@serialport+${suffix}@12.0.0/node_modules/@serialport"; - debugStore = pnpmStore + "/debug@4.3.4/node_modules"; - msStore = pnpmStore + "/ms@2.1.2/node_modules"; - nodeAddonApiStore = pnpmStore + "/node-addon-api@7.0.0/node_modules"; - nodeGypBuildStore = pnpmStore + "/node-gyp-build@4.6.0/node_modules"; - lodashEsStore = pnpmStore + "/lodash-es@4.17.23/node_modules"; - - # Fiat code per machine model + # Fiat code per machine model (for envTemplate display only) fiatCodeForModel = { douro = "GTQ"; tejo = "GTQ"; sintra = "EUR"; }.${machineModel} or "USD"; - atm-app = assert machineDir != "" - || throw "MACHINE_DIR env var must be set (use build-iso.sh or: export MACHINE_DIR=/path/to/apps/machine)"; - pkgs.runCommand "lamassu-atm-app" { } '' - mkdir -p $out/node_modules/@lamassu $out/node_modules/@serialport - cp -r ${builtins.path { path = machineDir + "/dist"; name = "dist"; }} $out/dist - cp -r ${builtins.path { path = machineDir + "/dist-electron"; name = "dist-electron"; }} $out/dist-electron - cp ${builtins.path { path = machineDir + "/package.json"; name = "package.json"; }} $out/package.json - - # Native modules required at runtime (not bundled by Vite) - # Resolved from pnpm virtual store to get real files (not symlinks) - cp -rL ${builtins.path { path = betterSqliteStore + "/better-sqlite3"; name = "better-sqlite3"; }} $out/node_modules/better-sqlite3 - cp -rL ${builtins.path { path = bindingsStore + "/bindings"; name = "bindings"; }} $out/node_modules/bindings - 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/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) - cp -rL ${builtins.path { path = serialportStore + "/serialport"; name = "serialport"; }} $out/node_modules/serialport - cp -rL ${builtins.path { path = serialportStreamStore + "/stream"; name = "serialport-stream"; }} $out/node_modules/@serialport/stream - # bindings-cpp: copy only dist/ + prebuilds/ + package.json (build/ has broken symlinks) - mkdir -p $out/node_modules/@serialport/bindings-cpp - cp -rL ${builtins.path { path = serialportBindingsCppStore + "/bindings-cpp/dist"; name = "serialport-bindings-cpp-dist"; }} $out/node_modules/@serialport/bindings-cpp/dist - cp -rL ${builtins.path { path = serialportBindingsCppStore + "/bindings-cpp/prebuilds"; name = "serialport-bindings-cpp-prebuilds"; }} $out/node_modules/@serialport/bindings-cpp/prebuilds - cp ${builtins.path { path = serialportBindingsCppStore + "/bindings-cpp/package.json"; name = "serialport-bindings-cpp-pkg"; }} $out/node_modules/@serialport/bindings-cpp/package.json - cp -rL ${builtins.path { path = serialportBindingsIfaceStore + "/bindings-interface"; name = "serialport-bindings-interface"; }} $out/node_modules/@serialport/bindings-interface - cp -rL ${builtins.path { path = serialportBindingMockStore + "/binding-mock"; name = "serialport-binding-mock"; }} $out/node_modules/@serialport/binding-mock - cp -rL ${builtins.path { path = debugStore + "/debug"; name = "debug"; }} $out/node_modules/debug - cp -rL ${builtins.path { path = msStore + "/ms"; name = "ms"; }} $out/node_modules/ms - cp -rL ${builtins.path { path = nodeAddonApiStore + "/node-addon-api"; name = "node-addon-api"; }} $out/node_modules/node-addon-api - cp -rL ${builtins.path { path = nodeGypBuildStore + "/node-gyp-build"; name = "node-gyp-build"; }} $out/node_modules/node-gyp-build - cp -rL ${builtins.path { path = lodashEsStore + "/lodash-es"; name = "lodash-es"; }} $out/node_modules/lodash-es - - # serialport parser sub-packages (barrel import requires all 10) - cp -rL ${builtins.path { path = parserStore "parser-byte-length" + "/parser-byte-length"; name = "parser-byte-length"; }} $out/node_modules/@serialport/parser-byte-length - cp -rL ${builtins.path { path = parserStore "parser-cctalk" + "/parser-cctalk"; name = "parser-cctalk"; }} $out/node_modules/@serialport/parser-cctalk - cp -rL ${builtins.path { path = parserStore "parser-delimiter" + "/parser-delimiter"; name = "parser-delimiter"; }} $out/node_modules/@serialport/parser-delimiter - cp -rL ${builtins.path { path = parserStore "parser-inter-byte-timeout" + "/parser-inter-byte-timeout"; name = "parser-inter-byte-timeout"; }} $out/node_modules/@serialport/parser-inter-byte-timeout - cp -rL ${builtins.path { path = parserStore "parser-packet-length" + "/parser-packet-length"; name = "parser-packet-length"; }} $out/node_modules/@serialport/parser-packet-length - cp -rL ${builtins.path { path = parserStore "parser-readline" + "/parser-readline"; name = "parser-readline"; }} $out/node_modules/@serialport/parser-readline - cp -rL ${builtins.path { path = parserStore "parser-ready" + "/parser-ready"; name = "parser-ready"; }} $out/node_modules/@serialport/parser-ready - cp -rL ${builtins.path { path = parserStore "parser-regex" + "/parser-regex"; name = "parser-regex"; }} $out/node_modules/@serialport/parser-regex - cp -rL ${builtins.path { path = parserStore "parser-slip-encoder" + "/parser-slip-encoder"; name = "parser-slip-encoder"; }} $out/node_modules/@serialport/parser-slip-encoder - cp -rL ${builtins.path { path = parserStore "parser-spacepacket" + "/parser-spacepacket"; name = "parser-spacepacket"; }} $out/node_modules/@serialport/parser-spacepacket - ''; - - # .env template with regtest defaults pointing to the dev machine + # .env template — runtime secrets are provisioned later via provision-atm.sh. + # Only non-secret defaults and display vars go here. envTemplate = pkgs.writeText "lamassu-atm-env" '' - 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 + VITE_RELAY_URL= + VITE_LIGHTNING_PUB_PUBKEY= + VITE_LIGHTNING_PUB_API_URL= + VITE_ADMIN_TOKEN= + VITE_ATM_PRIVATE_KEY= + VITE_EXTENSION_API_URL= + VITE_APP_ID= + VITE_LNDCONNECT_URL= VITE_LAMASSU_MACHINE_MODEL=${machineModel} VITE_LAMASSU_FIAT_CODE=${fiatCodeForModel} ELECTRON_FORCE_PROD=1 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..0172031 --- /dev/null +++ b/flake.lock @@ -0,0 +1,348 @@ +{ + "nodes": { + "cachix": { + "inputs": { + "devenv": [ + "devenv" + ], + "flake-compat": [ + "devenv", + "flake-compat" + ], + "git-hooks": [ + "devenv", + "git-hooks" + ], + "nixpkgs": [ + "devenv", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1760971495, + "narHash": "sha256-IwnNtbNVrlZIHh7h4Wz6VP0Furxg9Hh0ycighvL5cZc=", + "owner": "cachix", + "repo": "cachix", + "rev": "c5bfd933d1033672f51a863c47303fc0e093c2d2", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "latest", + "repo": "cachix", + "type": "github" + } + }, + "devenv": { + "inputs": { + "cachix": "cachix", + "flake-compat": "flake-compat", + "flake-parts": "flake-parts", + "git-hooks": "git-hooks", + "nix": "nix", + "nixd": "nixd", + "nixpkgs": [ + "nixpkgs-unstable" + ] + }, + "locked": { + "lastModified": 1772125979, + "narHash": "sha256-Le5RmbvN+wWnmoRmrtFMpWaNft3GnsFEymxq7asEPP8=", + "owner": "cachix", + "repo": "devenv", + "rev": "6ff63ee1aaec6710af8083561f95c33e4de63e14", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1761588595, + "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "devenv", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1760948891, + "narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-root": { + "locked": { + "lastModified": 1723604017, + "narHash": "sha256-rBtQ8gg+Dn4Sx/s+pvjdq3CB2wQNzx9XGFq/JVGCB6k=", + "owner": "srid", + "repo": "flake-root", + "rev": "b759a56851e10cb13f6b8e5698af7b59c44be26e", + "type": "github" + }, + "original": { + "owner": "srid", + "repo": "flake-root", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": [ + "devenv", + "flake-compat" + ], + "gitignore": "gitignore", + "nixpkgs": [ + "devenv", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1760663237, + "narHash": "sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "devenv", + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nix": { + "inputs": { + "flake-compat": [ + "devenv", + "flake-compat" + ], + "flake-parts": [ + "devenv", + "flake-parts" + ], + "git-hooks-nix": [ + "devenv", + "git-hooks" + ], + "nixpkgs": [ + "devenv", + "nixpkgs" + ], + "nixpkgs-23-11": [ + "devenv" + ], + "nixpkgs-regression": [ + "devenv" + ] + }, + "locked": { + "lastModified": 1771532737, + "narHash": "sha256-H26FQmOyvIGnedfAioparJQD8Oe+/byD6OpUpnI/hkE=", + "owner": "cachix", + "repo": "nix", + "rev": "7eb6c427c7a86fdc3ebf9e6cbf2a84e80e8974fd", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "devenv-2.32", + "repo": "nix", + "type": "github" + } + }, + "nixd": { + "inputs": { + "flake-parts": [ + "devenv", + "flake-parts" + ], + "flake-root": "flake-root", + "nixpkgs": [ + "devenv", + "nixpkgs" + ], + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1763964548, + "narHash": "sha256-JTRoaEWvPsVIMFJWeS4G2isPo15wqXY/otsiHPN0zww=", + "owner": "nix-community", + "repo": "nixd", + "rev": "d4bf15e56540422e2acc7bc26b20b0a0934e3f5e", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixd", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1735563628, + "narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1771923393, + "narHash": "sha256-Fy0+UXELv9hOE8WjYhJt8fMDLYTU2Dqn3cX4BwoGBos=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ea7f1f06811ce7fcc81d6c6fd4213150c23edcf2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "nixpkgs-unstable": "nixpkgs-unstable", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs-unstable" + ] + }, + "locked": { + "lastModified": 1772075164, + "narHash": "sha256-93XcvAt+6p7aAq1ERlxD2T17zLGoYGo64KJYasGcpgc=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "07601339b15fa6810541c0e7dc2f3664d92a7ad0", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "devenv", + "nixd", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1734704479, + "narHash": "sha256-MMi74+WckoyEWBRcg/oaGRvXC9BVVxDZNRMpL+72wBI=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "65712f5af67234dad91a5a4baee986a8b62dbf8f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index 678ed9a..ec3d050 100644 --- a/flake.nix +++ b/flake.nix @@ -2,85 +2,226 @@ description = "Lamassu Next - Nostr-Native Lightning ATM"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + # Stable NixOS for the ATM OS base + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; + + # 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"; + inputs.nixpkgs.follows = "nixpkgs-unstable"; }; + devenv = { url = "github:cachix/devenv"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "nixpkgs-unstable"; }; }; - outputs = { self, nixpkgs, flake-utils, rust-overlay, devenv }: - flake-utils.lib.eachDefaultSystem (system: - let - overlays = [ (import rust-overlay) ]; - pkgs = import nixpkgs { inherit system overlays; }; + outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils, rust-overlay, devenv }: + let + system = "x86_64-linux"; - rustToolchain = pkgs.rust-bin.stable.latest.default.override { - extensions = [ "rust-src" "rust-analyzer" ]; - targets = [ "wasm32-unknown-unknown" ]; + 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; + }; + + # Fiat code per machine model + fiatCodeForModel = { + douro = "GTQ"; + tejo = "GTQ"; + sintra = "EUR"; + }; + + 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 ]; + }; + + # 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/lamassu-atm.nix + ({ config, lib, pkgs, ... }: { + services.lamassu-atm = { + enable = true; + appDir = "${atm-app}"; + }; + + # Electron sandbox needs unprivileged user namespaces + boot.kernel.sysctl."kernel.unprivileged_userns_clone" = 1; + + # Env template — runtime secrets provisioned via provision-atm.sh + system.activationScripts.lamassu-env = '' + mkdir -p /var/lib/lamassu-atm + if [ ! -f /var/lib/lamassu-atm/.env ]; then + cp ${pkgs.writeText "lamassu-atm-env-default" '' + VITE_RELAY_URL= + VITE_LIGHTNING_PUB_PUBKEY= + VITE_LIGHTNING_PUB_API_URL= + VITE_ADMIN_TOKEN= + VITE_ATM_PRIVATE_KEY= + VITE_EXTENSION_API_URL= + VITE_APP_ID= + VITE_LNDCONNECT_URL= + VITE_LAMASSU_MACHINE_MODEL=${machineModel} + VITE_LAMASSU_FIAT_CODE=${fiatCode} + ELECTRON_FORCE_PROD=1 + DISPLAY=:0 + ''} /var/lib/lamassu-atm/.env + chmod 600 /var/lib/lamassu-atm/.env + chown lamassu:lamassu /var/lib/lamassu-atm/.env + fi + ''; + + # Override systemd service for Electron runtime + systemd.services.lamassu-atm = { + serviceConfig = { + EnvironmentFile = lib.mkForce "/var/lib/lamassu-atm/.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 --enable-logging ${atm-app}"; + 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 = [ "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'"; + }; + }; + + # SSH with password for initial provisioning + services.openssh.settings.PasswordAuthentication = lib.mkForce true; + }) + ]; + }; + 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"; + + # Backwards compat + 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; + + # UP Board (tejo) installed config + lamassu-atm = mkInstalledConfig "tejo" ./deploy/nixos/hardware/upboard.nix; + }; + + # ── Standalone NixOS module ─────────────────────────────────── + + nixosModules.default = import ./deploy/nixos/lamassu-atm.nix; + nixosModules.lamassu-atm = import ./deploy/nixos/lamassu-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"; }; + + # 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; + + # 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"; + }; + + # Backwards compat + iso = self.nixosConfigurations.douro.config.system.build.isoImage; + }; + } + // + # ── 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 { - # Development shell via devenv devShells.default = devenv.lib.mkShell { - inherit pkgs; + pkgs = dev-pkgs; modules = [ ./devenv.nix ]; }; - - # Packages - packages = { - # HAL Rust crate - hal = pkgs.rustPlatform.buildRustPackage { - pname = "lamassu-hal"; - version = "0.1.0"; - src = ./packages/hal; - cargoLock.lockFile = ./packages/hal/Cargo.lock; - - nativeBuildInputs = with pkgs; [ - pkg-config - ]; - - buildInputs = with pkgs; [ - openssl - libudev-zero - ]; - }; - - # Full application (TODO) - default = self.packages.${system}.hal; - }; - - # NixOS module for deployment - nixosModules.default = { config, lib, pkgs, ... }: { - options.services.lamassu-atm = { - enable = lib.mkEnableOption "Lamassu ATM service"; - - relayUrl = lib.mkOption { - type = lib.types.str; - description = "Nostr relay URL"; - }; - - lightningPubUrl = lib.mkOption { - type = lib.types.str; - description = "Lightning.Pub connection URL"; - }; - - identityPath = lib.mkOption { - type = lib.types.path; - default = "/etc/lamassu/machine.nsec"; - description = "Path to machine identity (nsec)"; - }; - }; - - config = lib.mkIf config.services.lamassu-atm.enable { - # TODO: systemd service, firewall rules, etc. - }; - }; } ); } diff --git a/nix/mkAtmApp.nix b/nix/mkAtmApp.nix new file mode 100644 index 0000000..6099dcd --- /dev/null +++ b/nix/mkAtmApp.nix @@ -0,0 +1,173 @@ +# Pure Nix derivation for the Lamassu ATM Electron app. +# +# Uses fetchPnpmDeps + pnpmConfigHook to build entirely inside the Nix sandbox, +# eliminating the need for --impure or a local pnpm install. +# +# Usage (from flake.nix): +# mkAtmApp = import ./nix/mkAtmApp.nix { inherit pkgs pkgs-unstable; src = self; }; +# atm-app-douro = mkAtmApp { model = "douro"; fiatCode = "GTQ"; }; + +{ pkgs, pkgs-unstable, src }: + +{ model, fiatCode }: + +let + nodejs = pkgs-unstable.nodejs_22; + pnpm = pkgs-unstable.pnpm_9; + electron = pkgs-unstable.electron; + + # Electron's Node.js headers — needed to compile native addons (better-sqlite3) + # that use V8 C++ API (not N-API). Must match the Electron version exactly. + electronHeaders = pkgs.fetchurl { + url = "https://electronjs.org/headers/v${electron.version}/node-v${electron.version}-headers.tar.gz"; + hash = "sha256-xtGHm/2Sb0ILU4yUVAqXTRLwzf8yiXxD0zIIrBk8NDw="; + }; +in +pkgs.stdenv.mkDerivation (finalAttrs: { + pname = "lamassu-atm-app"; + version = "0.1.0"; + + inherit src; + + # Filter to only the workspace packages needed for @lamassu/machine + pnpmWorkspaces = [ + "@lamassu/machine..." # "..." suffix = include transitive workspace deps + ]; + + pnpmDeps = pkgs-unstable.fetchPnpmDeps { + inherit (finalAttrs) pname version src pnpmWorkspaces; + inherit pnpm; + fetcherVersion = 3; + hash = "sha256-i8nFieH6VSibEDDDFtMqko1+JQ1MWxBCTNUJ6Jo8mE4="; + }; + + nativeBuildInputs = [ + nodejs + pnpm + pkgs-unstable.pnpmConfigHook + pkgs.python3 # node-gyp (better-sqlite3) + pkgs.pkg-config + pkgs.autoPatchelfHook # patch .node ELF binaries + ]; + + buildInputs = [ + pkgs.sqlite.dev # better-sqlite3 + pkgs.libudev-zero # serialport + pkgs.stdenv.cc.cc.lib # libstdc++ + ]; + + env = { + ELECTRON_SKIP_BINARY_DOWNLOAD = "1"; + # Vite compile-time variables (baked into the frontend bundle) + VITE_LAMASSU_MACHINE_MODEL = model; + VITE_LAMASSU_FIAT_CODE = fiatCode; + }; + + buildPhase = '' + runHook preBuild + + # Extract Electron's Node.js headers for native addon compilation. + # better-sqlite3 uses the V8 C++ API (not N-API), so it MUST be compiled + # against Electron's headers — not Node.js headers — or you get + # "undefined symbol: _ZN2v811HandleScopeC1EPNS_7IsolateE" at runtime. + electron_nodedir=$(mktemp -d) + tar -xzf ${electronHeaders} -C "$electron_nodedir" --strip-components=1 + + echo "=== Rebuilding better-sqlite3 against Electron ${electron.version} headers ===" + pushd node_modules/.pnpm/better-sqlite3@*/node_modules/better-sqlite3 + HOME=$TMPDIR ${nodejs}/bin/npx --yes node-gyp rebuild \ + --nodedir="$electron_nodedir" \ + --arch=x64 + popd + + # Build the Electron app (turbo builds all workspace deps + app) + pnpm --filter="@lamassu/machine..." build + + runHook postBuild + ''; + + # Cherry-pick only the runtime node_modules needed by the Electron app. + # Vite bundles most JS deps — these are native addons and dynamic imports + # that can't be bundled. + installPhase = '' + runHook preInstall + + mkdir -p $out/node_modules/{@lamassu,@serialport} + + # Helper: find a package dir inside the pnpm virtual store. + # pnpm store dirs look like: node_modules/.pnpm/@[_]/node_modules/ + # The glob must handle version + optional peer suffixes. + copy_pnpm_pkg() { + local pkg="$1" # e.g. "debug" or "@serialport/stream" + local dest="$2" # e.g. "$out/node_modules/debug" + # Convert @scope/name to @scope+name for the .pnpm dir prefix + local store_prefix="''${pkg/\//-}" # debug -> debug, @serialport/stream -> @serialport-stream + store_prefix="''${store_prefix//@/@}" + # Try globbing; sort to pick the first match deterministically + local found=$(find node_modules/.pnpm -maxdepth 1 -name "''${store_prefix}@*" -type d | sort | head -1) + if [ -z "$found" ]; then + # Fallback: scoped packages use + instead of - + store_prefix="''${pkg/\//+}" + found=$(find node_modules/.pnpm -maxdepth 1 -name "''${store_prefix}@*" -type d | sort | head -1) + fi + if [ -z "$found" ]; then + echo "ERROR: could not find pnpm store dir for $pkg" >&2 + exit 1 + fi + cp -rL "$found/node_modules/$pkg" "$dest" + } + + # Electron app build outputs + cp -r apps/machine/dist $out/dist + cp -r apps/machine/dist-electron $out/dist-electron + cp apps/machine/package.json $out/package.json + + # --- Native modules (not bundleable by Vite) --- + + # better-sqlite3 (compiled native addon) + copy_pnpm_pkg better-sqlite3 $out/node_modules/better-sqlite3 + copy_pnpm_pkg bindings $out/node_modules/bindings + copy_pnpm_pkg 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/dist + cp -rL packages/hal/dist/* $out/node_modules/@lamassu/hal/dist/ + cp packages/hal/package.json $out/node_modules/@lamassu/hal/package.json + + # serialport (native RS232 driver chain) + copy_pnpm_pkg serialport $out/node_modules/serialport + copy_pnpm_pkg @serialport/stream $out/node_modules/@serialport/stream + + # bindings-cpp: only dist/ + prebuilds/ + package.json (build/ has broken symlinks) + mkdir -p $out/node_modules/@serialport/bindings-cpp + bcpp_store=$(find node_modules/.pnpm -maxdepth 1 -name "@serialport+bindings-cpp@*" -type d | sort | head -1) + cp -rL "$bcpp_store/node_modules/@serialport/bindings-cpp/dist" $out/node_modules/@serialport/bindings-cpp/dist + cp -rL "$bcpp_store/node_modules/@serialport/bindings-cpp/prebuilds" $out/node_modules/@serialport/bindings-cpp/prebuilds + cp "$bcpp_store/node_modules/@serialport/bindings-cpp/package.json" $out/node_modules/@serialport/bindings-cpp/package.json + + copy_pnpm_pkg @serialport/bindings-interface $out/node_modules/@serialport/bindings-interface + copy_pnpm_pkg @serialport/binding-mock $out/node_modules/@serialport/binding-mock + + # Transitive deps of serialport + copy_pnpm_pkg debug $out/node_modules/debug + copy_pnpm_pkg ms $out/node_modules/ms + copy_pnpm_pkg node-addon-api $out/node_modules/node-addon-api + copy_pnpm_pkg node-gyp-build $out/node_modules/node-gyp-build + copy_pnpm_pkg lodash-es $out/node_modules/lodash-es + + # serialport parser sub-packages (barrel import requires all 10) + for parser in \ + parser-byte-length parser-cctalk parser-delimiter \ + parser-inter-byte-timeout parser-packet-length parser-readline \ + parser-ready parser-regex parser-slip-encoder parser-spacepacket; do + copy_pnpm_pkg "@serialport/$parser" "$out/node_modules/@serialport/$parser" + done + + runHook postInstall + ''; + + # autoPatchelfHook will scan $out for .node ELF binaries and patch their + # RPATH to find libstdc++, libudev, libsqlite3, etc. + # The musl prebuild ships alongside glibc — we only use glibc on NixOS. + autoPatchelfIgnoreMissingDeps = [ "libc.musl-x86_64.so.1" ]; +})