The atm-app's pnpm store is a fixed-output derivation keyed by this hash. pnpm-lock.yaml changed twice today (the dead-script dependency prune in763817b, then qrcode declared where fund-atm.ts uses it incbff654) and the hash was not updated, so nix reused the stale store and the sandboxed `pnpm install --offline` failed on @types/qrcode (ERR_PNPM_NO_OFFLINE_TARBALL). That is what sintra's nightly upgrade and the cachix push were dying on. Rule to carry forward: any commit that touches pnpm-lock.yaml must re-derive this hash (blank it, build, paste the `got:` value) — the local `pnpm build` passing says nothing about the nix build.
227 lines
10 KiB
Nix
227 lines
10 KiB
Nix
# Pure Nix derivation for the bitSpire 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 = "bitspire-atm-app";
|
|
version = "0.1.0";
|
|
|
|
inherit src;
|
|
|
|
# Filter to only the workspace packages needed for @bitSpire/machine
|
|
pnpmWorkspaces = [
|
|
"@bitSpire/machine..." # "..." suffix = include transitive workspace deps
|
|
];
|
|
|
|
pnpmDeps = pkgs-unstable.fetchPnpmDeps {
|
|
inherit (finalAttrs) pname version src pnpmWorkspaces;
|
|
inherit pnpm;
|
|
fetcherVersion = 3;
|
|
hash = "sha256-HIMjQx0REauknZkIU50M4KMU0lyNQ41UG6AUM/3Hn54=";
|
|
};
|
|
|
|
nativeBuildInputs = [
|
|
nodejs
|
|
pnpm
|
|
pkgs-unstable.pnpmConfigHook
|
|
# node-gyp@9.4.1 (pulled in by better-sqlite3) imports distutils, which
|
|
# Python 3.12 removed. pkgs.python3 became 3.12 in nixpkgs 24.11. Pin
|
|
# 3.11 here until pnpm-lock bumps better-sqlite3 to a node-gyp@10+ build.
|
|
pkgs.python311
|
|
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++
|
|
# @pokusew/pcsclite (nfc-pcsc): the `lib` output carries libpcsclite.so so
|
|
# autoPatchelf wires it into the .node RPATH at runtime. Compile/link paths
|
|
# are injected via CPATH/LIBRARY_PATH in buildPhase (its binding.gyp
|
|
# hardcodes Debian /usr paths instead of using pkg-config).
|
|
pkgs.pcsclite.lib
|
|
];
|
|
|
|
env = {
|
|
ELECTRON_SKIP_BINARY_DOWNLOAD = "1";
|
|
# Vite compile-time variables (baked into the frontend bundle)
|
|
VITE_BITSPIRE_MACHINE_MODEL = model;
|
|
VITE_BITSPIRE_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
|
|
|
|
# @pokusew/pcsclite (nfc-pcsc's native addon) — also V8 C++ API, so it too
|
|
# must be rebuilt against Electron's headers. Its binding.gyp hardcodes
|
|
# /usr/include/PCSC + /usr/lib, so point the compiler/linker at nixpkgs'
|
|
# pcsclite explicitly (winscard.h lives under include/PCSC).
|
|
echo "=== Rebuilding @pokusew/pcsclite against Electron ${electron.version} headers ==="
|
|
pushd node_modules/.pnpm/@pokusew+pcsclite@*/node_modules/@pokusew/pcsclite
|
|
CPATH="${pkgs.pcsclite.dev}/include/PCSC''${CPATH:+:$CPATH}" \
|
|
LIBRARY_PATH="${pkgs.pcsclite.lib}/lib''${LIBRARY_PATH:+:$LIBRARY_PATH}" \
|
|
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="@bitSpire/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,@pokusew}
|
|
|
|
# Helper: find a package dir inside the pnpm virtual store.
|
|
# pnpm store dirs look like: node_modules/.pnpm/<name>@<ver>[_<peer-suffix>]/node_modules/<name>
|
|
# 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
|
|
|
|
# nfc-pcsc + @pokusew/pcsclite (Bolt Card reader). The compiled
|
|
# pcsclite.node (from the rebuild above) rides along in the package dir and
|
|
# loads via `bindings` (already copied). autoPatchelf wires libpcsclite.
|
|
copy_pnpm_pkg nfc-pcsc $out/node_modules/nfc-pcsc
|
|
copy_pnpm_pkg @pokusew/pcsclite $out/node_modules/@pokusew/pcsclite
|
|
|
|
# @bitSpire/hal (workspace package, dynamically imported for hardware access)
|
|
mkdir -p $out/node_modules/@bitSpire/hal/dist
|
|
cp -rL packages/hal/dist/* $out/node_modules/@bitSpire/hal/dist/
|
|
cp packages/hal/package.json $out/node_modules/@bitSpire/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
|
|
|
|
# ── Strip node-gyp build detritus ──────────────────────────────────
|
|
# node-gyp leaves its scaffolding beside the compiled addons, and several
|
|
# of those files embed absolute /nix/store paths to the BUILD toolchain:
|
|
# build/node_gyp_bins/python3 an ELF copy of python3 with an RPATH
|
|
# build/config.gypi python3 + nodejs + npm paths
|
|
# build/Release/.deps/**.o.d pcsclite.dev include paths
|
|
# Nix scans $out for store hashes, so each becomes a RUNTIME reference and
|
|
# drags python311 + nodejs + npm + pcsclite.dev (~212MB of closure) onto
|
|
# every ATM. Nothing reads them at runtime — only build/Release/*.node is
|
|
# loaded, via `bindings` / `node-gyp-build`. Keep the addons, drop the
|
|
# scaffolding. obj.target/*.node is node-gyp's pre-copy of the same addon;
|
|
# the loaded one at build/Release/*.node is untouched.
|
|
find $out/node_modules -type d \
|
|
\( -name node_gyp_bins -o -name .deps -o -name obj.target -o -name obj \) \
|
|
-prune -exec rm -rf {} +
|
|
find $out/node_modules -path '*/build/*' -type f \
|
|
\( -name config.gypi -o -name '*.mk' -o -name Makefile \
|
|
-o -name binding.Makefile -o -name '*.a' -o -name '*.o' \) -delete
|
|
|
|
# pnpm/node-gyp rewrote these CLI helpers' shebangs to the build nodejs,
|
|
# which alone retains the full nodejs (not the slim one Electron needs).
|
|
# They are build-time utilities — the runtime entry of each package
|
|
# (index.js) carries no shebang — so point them at PATH instead of
|
|
# deleting files a package might still require.
|
|
find $out/node_modules -type f -name '*.js' \
|
|
-exec sed -i '1s|^#!/nix/store/[^ ]*/bin/node$|#!/usr/bin/env node|' {} +
|
|
|
|
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" ];
|
|
})
|