perf: cut the image 38% and turn GPU acceleration back on #112
5 changed files with 388 additions and 22 deletions
53
CLAUDE.md
53
CLAUDE.md
|
|
@ -4,7 +4,7 @@ Guidance for Claude Code when working in this repo. Read this before touching co
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
**bitSpire** is a Nostr-native Lightning ATM. Production ATMs (`batm3`, `douro`) currently run from `main` against Lightning.Pub; the `dev` branch — which is what this file describes — has been migrated to **LNbits over the nostr-native-transport**.
|
**bitSpire** is a Nostr-native Lightning ATM running **LNbits over the nostr-native-transport**. The `dev` branch, which this file describes, is what the machines run.
|
||||||
|
|
||||||
Core principles:
|
Core principles:
|
||||||
|
|
||||||
|
|
@ -25,8 +25,53 @@ bitSpire is an independent project under AGPL-3.0 and is not affiliated with Lam
|
||||||
|
|
||||||
## Branch model
|
## Branch model
|
||||||
|
|
||||||
- `main` — production. Lightning.Pub backend. The two production ATMs auto-pull from here daily at 04:00 (`flake.nix:152-160`). **DO NOT** push to `main` casually — a wrong commit gets baked into prod ATMs the next morning.
|
- `dev` — **what every live machine runs.** Not a staging branch any more. Verified
|
||||||
- `dev` — staging. LNbits backend. The Sintra dev unit auto-pulls from here (`?ref=dev` pin on this branch's `flake.nix`). Push freely; tag `pre-bitspire-cutover` is the rollback target if the migration ever needs to be reverted on prod.
|
2026-09-24 on batm3, whose `nixos-upgrade` unit pulls
|
||||||
|
`git+ssh://…/bitspire.git?ref=dev#batm3-installed` daily at 04:00. "Push freely
|
||||||
|
to dev" is no longer safe advice: a bad commit reaches production hardware the
|
||||||
|
next morning, unattended.
|
||||||
|
- `main` — Lightning.Pub era, historical. Tag `pre-bitspire-cutover` is the
|
||||||
|
rollback target if the migration ever has to be reverted.
|
||||||
|
|
||||||
|
> This section previously said the production ATMs ran `main` against
|
||||||
|
> Lightning.Pub and that only Sintra was on `dev`. That was stale and it was
|
||||||
|
> repeatedly taken at face value. Check the machine, not this file, before
|
||||||
|
> relying on which stack a given box runs: `systemctl cat nixos-upgrade` gives
|
||||||
|
> the branch, `/var/lib/bitspire` vs `/var/lib/lamassu-atm` gives the era.
|
||||||
|
|
||||||
|
### Fleet state (surveyed 2026-09-24)
|
||||||
|
|
||||||
|
| Machine | Reachable | Stack | GPU | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `sintra` | LAN `192.168.0.252` | dev / LNbits | Braswell `8086:22b0` → crocus | dev unit; ethernet `r8169` |
|
||||||
|
| `batm3` | wg `10.0.0.5` | dev / LNbits | Haswell GT2 `8086:0412` → crocus | **networks over WiFi**, `iwlwifi` 7260; ethernet down |
|
||||||
|
| `douro` | **down** | — | Bay Trail (Gen7) | needs reflashing with the current image and reconnecting to WireGuard |
|
||||||
|
| `tejo` | wg `10.0.0.3` | **Debian** (`ubilinux4`, kernel 4.9) | Braswell `8086:22b0` | never had bitspire installed; a flake target, not a deployment |
|
||||||
|
|
||||||
|
Two consequences worth holding onto. Every GPU in the fleet binds **crocus**, not
|
||||||
|
iris — sintra's Braswell does so despite being Gen8. And batm3's only working
|
||||||
|
network path is Intel WiFi, so `intel/iwlwifi` firmware is load-bearing there;
|
||||||
|
trimming it would strand the machine with no way back in.
|
||||||
|
|
||||||
|
### batm3's nightly upgrade is currently FAILING
|
||||||
|
|
||||||
|
Confirmed 2026-09-24. The run dies at:
|
||||||
|
|
||||||
|
```
|
||||||
|
04:03:26 building '…-bitspire-atm-app-0.1.0.drv'...
|
||||||
|
04:04:28 error: timed out after 60 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
The ATM app is built in-house and is **not in `aiolabs.cachix.org` or
|
||||||
|
`cache.nixos.org`**, so batm3 has to build it locally, and `nix.settings.timeout
|
||||||
|
= 60` in `flake.nix` kills it. The comment there assumes heavy derivations are
|
||||||
|
"effectively cache-only … upstream-cached", which is true of nixpkgs and false of
|
||||||
|
our own app.
|
||||||
|
|
||||||
|
So the machine is pinned to whatever generation last succeeded, and nothing
|
||||||
|
merged to `dev` reaches it. This is the same class of silent-updater failure as
|
||||||
|
#98, in a new form. The fix is pushing `atm-app-*` to the aiolabs cachix as part
|
||||||
|
of releasing, not raising the timeout — a 60s ceiling on ATM hardware is correct.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|
@ -220,7 +265,7 @@ UP Board enumerates its eMMC controller via ACPI, not PCI. `upboard.nix` force-l
|
||||||
|
|
||||||
- The renderer logs prefix every line with a tag: `[Lightning]`, `[ATM]`, `[ATM Service]`, `[LNURL Session]`, `[CLINK]`, `[StateStore]`. `journalctl -u bitspire | grep '\['` is your friend.
|
- The renderer logs prefix every line with a tag: `[Lightning]`, `[ATM]`, `[ATM Service]`, `[LNURL Session]`, `[CLINK]`, `[StateStore]`. `journalctl -u bitspire | grep '\['` is your friend.
|
||||||
- **Never pass an object as a console argument in the renderer.** Electron's console bridge stringifies each argument, so `console.log('msg:', { a, b })` reaches the journal as `msg: [object Object]` and every field is lost. Interpolate instead. Cost a debugging session on 2026-09-23, when a cassette publish that had worked looked like it had done nothing.
|
- **Never pass an object as a console argument in the renderer.** Electron's console bridge stringifies each argument, so `console.log('msg:', { a, b })` reaches the journal as `msg: [object Object]` and every field is lost. Interpolate instead. Cost a debugging session on 2026-09-23, when a cassette publish that had worked looked like it had done nothing.
|
||||||
- `bitspire.service` runs as the `lamassu` user; `/var/lib/bitspire` is its `dataDir` (ReadWritePaths). DB lives at `/var/lib/bitspire/state.db` (we previously had `/var/lib/lamassu-atm` — that path is gone on dev, see commit `9c455d6`).
|
- `bitspire.service` runs as the `bitspire` user (verified on sintra 2026-09-24; this line used to say `lamassu`, left over from the rename in `46e52f6`); `/var/lib/bitspire` is its `dataDir` (ReadWritePaths). DB lives at `/var/lib/bitspire/state.db` (we previously had `/var/lib/lamassu-atm` — that path is gone on dev, see commit `9c455d6`).
|
||||||
- The `lightning.lightningPub` field on `LightningServices` is a `LightningBackend` *adapter*, not a `LightningPubClient`. Don't try to call LP-only methods on it.
|
- The `lightning.lightningPub` field on `LightningServices` is a `LightningBackend` *adapter*, not a `LightningPubClient`. Don't try to call LP-only methods on it.
|
||||||
|
|
||||||
## Related documentation
|
## Related documentation
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,122 @@
|
||||||
|
|
||||||
{ config, lib, pkgs, pkgs-unstable, ... }:
|
{ config, lib, pkgs, pkgs-unstable, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
# ── Firmware pruning (bitspire#70 sizing) ────────────────────────────
|
||||||
|
# hardware.enableRedistributableFirmware installs the entire linux-firmware
|
||||||
|
# tree: 752MB compressed, 16% of the image and its single largest item. The
|
||||||
|
# fleet is four fixed Intel boards. The other ~640MB is firmware for
|
||||||
|
# Qualcomm, Mellanox, NVIDIA, Marvell, AMD and MediaTek parts that will
|
||||||
|
# never appear in one of these machines.
|
||||||
|
#
|
||||||
|
# Keep only what a bitSpire board can plausibly load. Entries are paths
|
||||||
|
# inside lib/firmware; nothing outside this list is copied.
|
||||||
|
firmwareKeep = [
|
||||||
|
# Intel GPU. Gen9 (Apollo Lake) loads DMC from here. Bay Trail and
|
||||||
|
# Haswell load nothing, but 9.6MB is cheap insurance against a board swap.
|
||||||
|
"i915"
|
||||||
|
# Intel WiFi, 89MB and the bulk of what survives, covering every Intel
|
||||||
|
# card since 2008. This is the conservative half of the trade: losing the
|
||||||
|
# network on a deployed ATM is not remotely recoverable. Narrow it to the
|
||||||
|
# specific generation once each machine's card is known, via
|
||||||
|
# `lspci -k | grep -A3 Network` on the box.
|
||||||
|
"intel/iwlwifi"
|
||||||
|
"rtl_nic" # Realtek GbE (r8169) — the UP Board's onboard NIC
|
||||||
|
"rtw88" # Realtek WiFi, the usual M.2 or USB retrofit
|
||||||
|
"rtw89"
|
||||||
|
"brcm" # Broadcom WiFi, the other usual retrofit
|
||||||
|
# Intel Smart Sound Technology DSP, 420KB. Cherry Trail boards (sintra,
|
||||||
|
# tejo) probe intel_sst_acpi at boot whether or not anything will use the
|
||||||
|
# audio, and without the blob every boot logs
|
||||||
|
# Direct firmware load for intel/fw_sst_22a8.bin failed with error -2
|
||||||
|
# Found by pruning, rebooting sintra and reading dmesg. The audio stack is
|
||||||
|
# gone so this changes no behaviour, but a recurring error in a payment
|
||||||
|
# terminal's boot log is worth 420KB to remove: an error people learn to
|
||||||
|
# ignore is one they will ignore when it matters.
|
||||||
|
"intel/fw_sst_0f28.bin"
|
||||||
|
"intel/fw_sst_0f28_ssp0.bin"
|
||||||
|
"intel/fw_sst_22a8.bin"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Prune the tree rather than hand-pick files, so a firmware bump can't
|
||||||
|
# silently drop a blob we depend on. Left UNCOMPRESSED on purpose: NixOS
|
||||||
|
# compresses each hardware.firmware entry itself, zstd or xz depending on
|
||||||
|
# what the machine's kernel understands, and douro's 5.15 predates zstd
|
||||||
|
# firmware support. Pre-compressing here would hand douro a tree it cannot
|
||||||
|
# read.
|
||||||
|
bitspireFirmware = pkgs.runCommand "linux-firmware-bitspire"
|
||||||
|
{
|
||||||
|
inherit (pkgs.linux-firmware) version;
|
||||||
|
meta = pkgs.linux-firmware.meta // {
|
||||||
|
description = "linux-firmware pruned to the hardware bitSpire ships on";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
''
|
||||||
|
src=${pkgs.linux-firmware}/lib/firmware
|
||||||
|
dst=$out/lib/firmware
|
||||||
|
mkdir -p "$dst"
|
||||||
|
|
||||||
|
for p in ${lib.escapeShellArgs firmwareKeep}; do
|
||||||
|
if [ ! -e "$src/$p" ]; then
|
||||||
|
echo "ERROR: firmwareKeep entry '$p' is not in linux-firmware" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p "$dst/$(dirname "$p")"
|
||||||
|
cp -a "$src/$p" "$dst/$p"
|
||||||
|
done
|
||||||
|
|
||||||
|
# A kept directory can contain symlinks pointing at blobs OUTSIDE it:
|
||||||
|
# brcm/brcmfmac*.bin are links into cypress/, for instance. Left dangling
|
||||||
|
# they fail nixpkgs' firmware compression step, and silently deleting
|
||||||
|
# them would quietly drop firmware a device needs. So pull the targets in
|
||||||
|
# instead. Looped because a resolved target can itself be a link.
|
||||||
|
for _pass in 1 2 3; do
|
||||||
|
_pulled=0
|
||||||
|
while IFS= read -r link; do
|
||||||
|
tgt=$(readlink -m "$link")
|
||||||
|
case "$tgt" in
|
||||||
|
"$dst"/*) rel=''${tgt#"$dst"/} ;;
|
||||||
|
*) continue ;;
|
||||||
|
esac
|
||||||
|
if [ ! -e "$dst/$rel" ] && [ -e "$src/$rel" ]; then
|
||||||
|
mkdir -p "$dst/$(dirname "$rel")"
|
||||||
|
cp -a "$src/$rel" "$dst/$rel"
|
||||||
|
_pulled=1
|
||||||
|
fi
|
||||||
|
done < <(find "$dst" -xtype l)
|
||||||
|
[ "$_pulled" -eq 0 ] && break
|
||||||
|
done
|
||||||
|
|
||||||
|
# Anything still dangling is not in linux-firmware at all. Fail loudly
|
||||||
|
# rather than ship a tree with holes in it.
|
||||||
|
if find "$dst" -xtype l | grep -q .; then
|
||||||
|
echo "ERROR: dangling firmware symlinks after resolution:" >&2
|
||||||
|
find "$dst" -xtype l >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# linux-firmware stores many blobs under a vendor directory and leaves a
|
||||||
|
# flat top-level symlink pointing at them, e.g.
|
||||||
|
# iwlwifi-cc-a0-77.ucode -> intel/iwlwifi/iwlwifi-cc-a0-77.ucode. The
|
||||||
|
# kernel requests the flat name, so a kept blob is useless without its
|
||||||
|
# link. Recreate every top-level link whose target survived the prune.
|
||||||
|
( cd "$src"
|
||||||
|
find . -maxdepth 1 -type l -printf '%f\t%l\n' \
|
||||||
|
| while IFS="$(printf '\t')" read -r link target; do
|
||||||
|
# if/then, not `[ ... ] && ln`: the latter makes the loop's exit
|
||||||
|
# status depend on whether the LAST candidate matched, and a
|
||||||
|
# non-match returns 1, which set -e turns into a build failure.
|
||||||
|
# Whether it fails is then a function of readdir order.
|
||||||
|
if [ -e "$dst/$target" ]; then
|
||||||
|
ln -s "$target" "$dst/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "firmware kept: $(find "$dst" -type f | wc -l) files, \
|
||||||
|
$(find "$dst" -type l | wc -l) links, $(du -sh "$dst" | cut -f1) uncompressed"
|
||||||
|
'';
|
||||||
|
in
|
||||||
{
|
{
|
||||||
# System basics
|
# System basics
|
||||||
system.stateVersion = "24.05";
|
system.stateVersion = "24.05";
|
||||||
|
|
@ -13,10 +129,130 @@
|
||||||
# - speechd: text-to-speech (speech-dispatcher → espeak-ng → mbrola, ~1GB).
|
# - speechd: text-to-speech (speech-dispatcher → espeak-ng → mbrola, ~1GB).
|
||||||
# An ATM does not talk.
|
# An ATM does not talk.
|
||||||
# - documentation: man/info/NixOS manual — no one reads them on a kiosk.
|
# - documentation: man/info/NixOS manual — no one reads them on a kiosk.
|
||||||
|
# - pipewire: the audio stack (+ WirePlumber, ALSA, the PulseAudio shim),
|
||||||
|
# ~353MB. The app has never played a sound — nothing under apps/machine or
|
||||||
|
# packages/ constructs an Audio element or ships an audio file.
|
||||||
|
#
|
||||||
|
# All three need mkForce, not just an absent/false assignment: enabling
|
||||||
|
# services.xserver pulls in NixOS's `graphical-desktop` module, which
|
||||||
|
# mkDefault-enables speechd AND pipewire (services/misc/graphical-desktop.nix).
|
||||||
|
# Dropping our own `enable = true` simply falls back to that default — the
|
||||||
|
# 353MB stayed until this was forced off. Re-enable pipewire (with alsa +
|
||||||
|
# pulse) and security.rtkit if transaction sounds are ever added.
|
||||||
services.speechd.enable = lib.mkForce false;
|
services.speechd.enable = lib.mkForce false;
|
||||||
|
services.pipewire.enable = lib.mkForce false;
|
||||||
documentation.enable = false;
|
documentation.enable = false;
|
||||||
documentation.nixos.enable = false;
|
documentation.nixos.enable = false;
|
||||||
|
|
||||||
|
# Ship the pruned firmware tree instead of all of linux-firmware. mkForce
|
||||||
|
# because every hardware/*.nix sets enableRedistributableFirmware = true;
|
||||||
|
# overriding once here keeps the four machines in step. Turning that option
|
||||||
|
# off also drops the extras it bundles (sof-firmware, libreelec-dvb,
|
||||||
|
# alsa-firmware, intel2200BG, zd1211fw and friends), none of which applies to
|
||||||
|
# a soundless kiosk on a wired Intel board. The regulatory database is
|
||||||
|
# normally implied by the same option, so ask for it explicitly: without it
|
||||||
|
# WiFi is pinned to the most restrictive channel set.
|
||||||
|
hardware.enableRedistributableFirmware = lib.mkForce false;
|
||||||
|
hardware.wirelessRegulatoryDatabase = true;
|
||||||
|
hardware.firmware = [ bitspireFirmware ];
|
||||||
|
|
||||||
|
# Make the prune stick. Without this, a nixpkgs bump or a stray module
|
||||||
|
# setting enableRedistributableFirmware back to true silently re-adds 750MB
|
||||||
|
# and nobody notices until an eMMC runs out of room at 04:00. The regex
|
||||||
|
# matches the upstream package's versioned name (linux-firmware-20260519)
|
||||||
|
# and deliberately not ours (linux-firmware-bitspire), so the pruned tree
|
||||||
|
# passes and the full one fails the build with a readable error.
|
||||||
|
system.forbiddenDependenciesRegexes = [ "linux-firmware-[0-9]" ];
|
||||||
|
|
||||||
|
# Mesa without an LLVM-backed rasterizer.
|
||||||
|
#
|
||||||
|
# nixpkgs builds Mesa with 21 gallium drivers. Two of them, llvmpipe and
|
||||||
|
# radeonsi, link LLVM, and that RPATH pulls llvm-21-lib into the system
|
||||||
|
# closure: 540MB, a ninth of the image, on a kiosk with a soldered Intel GPU.
|
||||||
|
#
|
||||||
|
# The driver list has to span three Intel generations:
|
||||||
|
# crocus EVERY machine in the fleet. Surveyed, not assumed: sintra
|
||||||
|
# and tejo are Braswell [8086:22b0], batm3 is Haswell GT2
|
||||||
|
# [8086:0412], douro is Bay Trail. sintra and batm3 were read
|
||||||
|
# straight off their running X logs; both say crocus.
|
||||||
|
# i915 pre-Gen4, insurance against an older board turning up.
|
||||||
|
# softpipe the software rasterizer that does NOT use LLVM. Kept so a
|
||||||
|
# board whose KMS driver fails still brings up X, slowly,
|
||||||
|
# rather than dying headless in the field.
|
||||||
|
#
|
||||||
|
# ── IRIS IS DELIBERATELY ABSENT AND RE-ADDING IT COSTS 540MB ────────
|
||||||
|
# iris covers Gen8+ big-core Intel, which nothing here has. Its absence is
|
||||||
|
# what lets -Dllvm=disabled below work: mesa's meson puts
|
||||||
|
# with_gallium_iris in with_driver_using_cl and then
|
||||||
|
# with_llvm.enable_if(with_clc, 'CLC requires LLVM')
|
||||||
|
# so asking for iris drags in the OpenCL frontend and the whole of
|
||||||
|
# llvm-lib. Mesa's closure is 88MB without iris, 633MB with.
|
||||||
|
#
|
||||||
|
# A newer x86 board — a modern NUC, the "build it from these parts" kiosk
|
||||||
|
# — WILL need iris. Until one exists, such a board falls back to softpipe
|
||||||
|
# and renders in software: it boots, it displays, it looks fine, and it is
|
||||||
|
# very slow. Check `DRI driver:` in /var/log/X.0.log on any new hardware
|
||||||
|
# rather than assuming this list still covers it.
|
||||||
|
# i915 pre-Gen4, insurance against an older board turning up
|
||||||
|
# softpipe the software rasterizer that does NOT use LLVM. Kept so a
|
||||||
|
# board whose KMS driver fails still brings up X, slowly,
|
||||||
|
# rather than dying headless in the field. This is the role
|
||||||
|
# llvmpipe was playing, for 540MB.
|
||||||
|
#
|
||||||
|
# Vulkan is emptied because nothing here uses it, and its software ICD
|
||||||
|
# (lavapipe) is the other LLVM consumer. The VDPAU and VA state trackers
|
||||||
|
# have to go with it: meson refuses to build them unless one of the AMD or
|
||||||
|
# NVIDIA gallium drivers is present. Intel VA-API is unaffected, it comes
|
||||||
|
# from intel-media-driver in hardware/*.nix.
|
||||||
|
hardware.graphics.package =
|
||||||
|
(pkgs.mesa.override {
|
||||||
|
galliumDrivers = [ "crocus" "i915" "softpipe" ];
|
||||||
|
vulkanDrivers = [ ];
|
||||||
|
vulkanLayers = [ ];
|
||||||
|
}).overrideAttrs
|
||||||
|
(old: {
|
||||||
|
mesonFlags = old.mesonFlags ++ [
|
||||||
|
# Severs LLVM outright. Only possible because iris is out of the
|
||||||
|
# driver list above; with iris present meson refuses this flag.
|
||||||
|
# Verified with patchelf: libgallium.so ends up with no libLLVM in
|
||||||
|
# its DT_NEEDED, not merely absent from the closure listing.
|
||||||
|
# Dropping llvmpipe alone never achieved this.
|
||||||
|
(lib.mesonEnable "llvm" false)
|
||||||
|
(lib.mesonBool "gallium-rusticl" false)
|
||||||
|
# nixpkgs builds the asahi/panfrost cross tools and installs
|
||||||
|
# mesa-clc on native builds. Both reference prog_mesa_clc, which
|
||||||
|
# exists only when CLC is on, so they go with LLVM. An x86 kiosk
|
||||||
|
# has no use for either.
|
||||||
|
(lib.mesonOption "tools" "")
|
||||||
|
(lib.mesonBool "install-mesa-clc" false)
|
||||||
|
(lib.mesonBool "install-precomp-compiler" false)
|
||||||
|
(lib.mesonEnable "gallium-vdpau" false)
|
||||||
|
(lib.mesonEnable "gallium-va" false)
|
||||||
|
(lib.mesonEnable "intel-rt" false)
|
||||||
|
];
|
||||||
|
# Mesa declares spirv2dxil and cross_tools as outputs unconditionally,
|
||||||
|
# but they only receive files when the d3d12, asahi or panfrost gallium
|
||||||
|
# drivers are built, and none of those are in the list above. Nix fails
|
||||||
|
# a build that leaves a declared output unproduced, so create them
|
||||||
|
# empty. (Mesa sets __structuredAttrs, so $outputs is a bash array and
|
||||||
|
# a plain `for o in $outputs` loop silently does nothing here.)
|
||||||
|
postInstall = (old.postInstall or "") + ''
|
||||||
|
mkdir -p "$spirv2dxil" "$cross_tools" "$opencl"
|
||||||
|
'';
|
||||||
|
|
||||||
|
# With rusticl off there is no libRusticlOpenCL.so, and Mesa's
|
||||||
|
# postFixup patchelfs it unconditionally. Drop just that argument.
|
||||||
|
# The assert makes a nixpkgs bump that reshapes this line fail loudly
|
||||||
|
# here rather than silently stop removing LLVM.
|
||||||
|
postFixup =
|
||||||
|
let
|
||||||
|
marker = " $opencl/lib/libRusticlOpenCL.so";
|
||||||
|
in
|
||||||
|
assert lib.assertMsg (lib.hasInfix marker old.postFixup)
|
||||||
|
"mesa postFixup no longer patchelfs libRusticlOpenCL.so; revisit this override";
|
||||||
|
lib.replaceStrings [ marker ] [ "" ] old.postFixup;
|
||||||
|
});
|
||||||
|
|
||||||
# Networking
|
# Networking
|
||||||
networking = {
|
networking = {
|
||||||
hostName = "bitspire";
|
hostName = "bitspire";
|
||||||
|
|
@ -99,38 +335,38 @@
|
||||||
user = "bitspire";
|
user = "bitspire";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Audio (for transaction sounds)
|
|
||||||
security.rtkit.enable = true;
|
|
||||||
services.pipewire = {
|
|
||||||
enable = true;
|
|
||||||
alsa.enable = true;
|
|
||||||
pulse.enable = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
# System packages
|
# System packages
|
||||||
|
#
|
||||||
|
# Kept deliberately thin — this is a kiosk, and every entry here is closure
|
||||||
|
# that ships to each ATM and eats eMMC headroom the nightly rebuild needs.
|
||||||
|
# Deliberately absent (see #70 sizing):
|
||||||
|
# git 70MB. nixos-rebuild fetches the flake with its OWN git-minimal,
|
||||||
|
# which stays in the closure via unit-nixos-upgrade.service, so
|
||||||
|
# auto-upgrade is unaffected.
|
||||||
|
# vim 43MB. Replaced by nano — an on-box editor is worth a few MB for
|
||||||
|
# field edits to /var/lib/bitspire/.env, vim's bulk is not.
|
||||||
|
# nodejs_22 94MB. Nothing runs it: the app is Electron (which embeds its
|
||||||
|
# own node) and fund-atm already pins pkgs-unstable.nodejs itself.
|
||||||
|
# wget curl covers it.
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
# System utilities
|
# System utilities
|
||||||
htop
|
htop
|
||||||
vim
|
nano
|
||||||
git
|
|
||||||
curl
|
curl
|
||||||
wget
|
|
||||||
|
|
||||||
# Hardware debugging
|
# Hardware debugging
|
||||||
usbutils
|
usbutils
|
||||||
pciutils
|
pciutils
|
||||||
lsof
|
lsof
|
||||||
|
|
||||||
# Serial port tools
|
# Serial port tools (validator/dispenser live on ttyJ5/ttyJ7 — these are
|
||||||
|
# how a field fault gets diagnosed, and they cost ~2MB between them)
|
||||||
minicom
|
minicom
|
||||||
screen
|
screen
|
||||||
|
|
||||||
# For the Electron app
|
# For the Electron app
|
||||||
pkgs-unstable.electron
|
pkgs-unstable.electron
|
||||||
|
|
||||||
# Node.js for the application
|
|
||||||
pkgs-unstable.nodejs_22
|
|
||||||
|
|
||||||
# Camera support. v4l-utils' default build drags in the whole Qt6 stack
|
# Camera support. v4l-utils' default build drags in the whole Qt6 stack
|
||||||
# for its qv4l2 GUI (~0.5GB) — we only ever use the v4l2-ctl CLI, so drop
|
# for its qv4l2 GUI (~0.5GB) — we only ever use the v4l2-ctl CLI, so drop
|
||||||
# the GUI.
|
# the GUI.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
# Does NOT import hardware/upboard.nix (its fileSystems conflict with live boot).
|
# Does NOT import hardware/upboard.nix (its fileSystems conflict with live boot).
|
||||||
# Instead, duplicates only the hardware-relevant kernel modules and GPU config.
|
# Instead, duplicates only the hardware-relevant kernel modules and GPU config.
|
||||||
|
|
||||||
{ config, lib, pkgs, pkgs-unstable, nixpkgs, machineModel ? "douro", atm-app, ... }:
|
{ config, lib, pkgs, pkgs-unstable, nixpkgs, machineModel ? "douro", atm-app, kioskLauncher, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
# Fiat code per machine model (for envTemplate display only)
|
# Fiat code per machine model (for envTemplate display only)
|
||||||
|
|
@ -162,7 +162,7 @@ in
|
||||||
Environment = "LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib";
|
Environment = "LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib";
|
||||||
# Electron needs --no-sandbox in the live/testing environment
|
# Electron needs --no-sandbox in the live/testing environment
|
||||||
# --enable-logging makes renderer console.log visible in journalctl
|
# --enable-logging makes renderer console.log visible in journalctl
|
||||||
ExecStart = lib.mkForce "${pkgs-unstable.electron}/bin/electron --no-sandbox --disable-gpu-sandbox --disable-gpu --disable-software-rasterizer --enable-logging ${atm-app}";
|
ExecStart = lib.mkForce "${kioskLauncher}";
|
||||||
# Prevent Electron from consuming all RAM on memory-constrained ATMs
|
# Prevent Electron from consuming all RAM on memory-constrained ATMs
|
||||||
MemoryMax = lib.mkForce "1G";
|
MemoryMax = lib.mkForce "1G";
|
||||||
# Disable all security hardening that conflicts with Electron
|
# Disable all security hardening that conflicts with Electron
|
||||||
|
|
|
||||||
60
flake.nix
60
flake.nix
|
|
@ -53,6 +53,55 @@
|
||||||
overlays = [ (import rust-overlay) ];
|
overlays = [ (import rust-overlay) ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Kiosk launcher. The GPU-related Electron flags sit in a shell variable
|
||||||
|
# rather than being baked into ExecStart, so they can be changed on a
|
||||||
|
# running machine by editing /var/lib/bitspire/.env and restarting the
|
||||||
|
# unit. No rebuild, no reboot, and a bad value is one edit away from
|
||||||
|
# being undone — which matters on a box whose screen nobody can see.
|
||||||
|
#
|
||||||
|
# THE DEFAULT IS NOW HARDWARE ACCELERATION.
|
||||||
|
#
|
||||||
|
# From the first ISO commit (19d43c2) until today the kiosk launched with
|
||||||
|
# --disable-gpu AND --disable-software-rasterizer, which turns off GPU
|
||||||
|
# compositing and the SwiftShader fallback together and leaves Chromium
|
||||||
|
# rasterising every pixel on the CPU. Nothing in git ever justified the
|
||||||
|
# pair: no comment, no issue, no commit message. Meanwhile the
|
||||||
|
# descriptive config at /etc/bitspire/config.env claimed
|
||||||
|
# ELECTRON_DISABLE_GPU=false, contradicting the actual command line.
|
||||||
|
#
|
||||||
|
# Tested on sintra 2026-09-24. With the flags removed the GPU process is
|
||||||
|
# stable (zero crashes, zero service restarts) and genuinely on hardware
|
||||||
|
# — /proc/<gpu-pid>/maps shows libgallium, libGLX_mesa and dri_gbm, with
|
||||||
|
# no swrast and no SwiftShader — rendering through crocus on Braswell.
|
||||||
|
# Confirmed by eye on the panel.
|
||||||
|
#
|
||||||
|
# DOURO IS EXEMPT. It keeps the old flags. Bay Trail carries three
|
||||||
|
# separate display workarounds already — a 5.15 kernel pin for an i915
|
||||||
|
# eDP regression, i915.enable_psr=0, and vt.handoff=7 to preserve the
|
||||||
|
# BIOS display init — so it is the most plausible machine for the
|
||||||
|
# original flags to have been a real fix rather than scaffolding. It is
|
||||||
|
# also down pending a reflash, so it cannot be tested. Drop this
|
||||||
|
# exemption once douro is back and accelerates cleanly.
|
||||||
|
#
|
||||||
|
# To override per machine, in /var/lib/bitspire/.env:
|
||||||
|
# BITSPIRE_ELECTRON_GPU_FLAGS= acceleration
|
||||||
|
# BITSPIRE_ELECTRON_GPU_FLAGS=--disable-gpu no GPU
|
||||||
|
# BITSPIRE_ELECTRON_GPU_FLAGS=--use-gl=egl force EGL
|
||||||
|
# (line absent) model default
|
||||||
|
#
|
||||||
|
# Note `-` and not `:-`: an explicitly EMPTY value means "no GPU flags at
|
||||||
|
# all", and must not fall back to the default. Unquoted on purpose so the
|
||||||
|
# value word-splits into argv.
|
||||||
|
mkKioskLauncher = machineModel: atm-app: pkgs.writeShellScript "bitspire-kiosk" ''
|
||||||
|
default_gpu_flags="${
|
||||||
|
if machineModel == "douro" then "--disable-gpu --disable-software-rasterizer" else ""
|
||||||
|
}"
|
||||||
|
exec ${pkgs-unstable.electron}/bin/electron \
|
||||||
|
--no-sandbox --disable-gpu-sandbox --enable-logging \
|
||||||
|
''${BITSPIRE_ELECTRON_GPU_FLAGS-$default_gpu_flags} \
|
||||||
|
${atm-app}
|
||||||
|
'';
|
||||||
|
|
||||||
# Pure ATM app builder (no --impure needed)
|
# Pure ATM app builder (no --impure needed)
|
||||||
mkAtmApp = import ./nix/mkAtmApp.nix {
|
mkAtmApp = import ./nix/mkAtmApp.nix {
|
||||||
inherit pkgs pkgs-unstable;
|
inherit pkgs pkgs-unstable;
|
||||||
|
|
@ -81,6 +130,7 @@
|
||||||
inherit system;
|
inherit system;
|
||||||
specialArgs = {
|
specialArgs = {
|
||||||
inherit pkgs-unstable nixpkgs machineModel atm-app;
|
inherit pkgs-unstable nixpkgs machineModel atm-app;
|
||||||
|
kioskLauncher = mkKioskLauncher machineModel atm-app;
|
||||||
};
|
};
|
||||||
modules = [
|
modules = [
|
||||||
./deploy/nixos/live.nix
|
./deploy/nixos/live.nix
|
||||||
|
|
@ -209,6 +259,14 @@
|
||||||
VITE_SPIRE_SEED=
|
VITE_SPIRE_SEED=
|
||||||
ELECTRON_FORCE_PROD=1
|
ELECTRON_FORCE_PROD=1
|
||||||
DISPLAY=:0
|
DISPLAY=:0
|
||||||
|
# Uncomment to change Electron's GPU flags without a
|
||||||
|
# rebuild, then `systemctl restart bitspire`. An empty
|
||||||
|
# value means full GPU acceleration; the line being absent
|
||||||
|
# means the shipped default (GPU and software rasterizer
|
||||||
|
# both off). Commented rather than set, because a present
|
||||||
|
# -but-empty value here would silently enable the GPU on
|
||||||
|
# every machine that regenerates its .env.
|
||||||
|
# BITSPIRE_ELECTRON_GPU_FLAGS=
|
||||||
'' + pkgs.lib.optionalString (config.services.bitspire.relayUrl != "") ''
|
'' + pkgs.lib.optionalString (config.services.bitspire.relayUrl != "") ''
|
||||||
VITE_RELAY_URL=${config.services.bitspire.relayUrl}
|
VITE_RELAY_URL=${config.services.bitspire.relayUrl}
|
||||||
'' + pkgs.lib.optionalString (config.services.bitspire.lnbitsServerPubkey != "") ''
|
'' + pkgs.lib.optionalString (config.services.bitspire.lnbitsServerPubkey != "") ''
|
||||||
|
|
@ -224,7 +282,7 @@
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
EnvironmentFile = lib.mkForce "/var/lib/bitspire/.env";
|
EnvironmentFile = lib.mkForce "/var/lib/bitspire/.env";
|
||||||
Environment = "LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib";
|
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}";
|
ExecStart = lib.mkForce "${mkKioskLauncher machineModel atm-app}";
|
||||||
MemoryMax = lib.mkForce "1G";
|
MemoryMax = lib.mkForce "1G";
|
||||||
NoNewPrivileges = lib.mkForce false;
|
NoNewPrivileges = lib.mkForce false;
|
||||||
ProtectSystem = lib.mkForce false;
|
ProtectSystem = lib.mkForce false;
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,33 @@ pkgs.stdenv.mkDerivation (finalAttrs: {
|
||||||
copy_pnpm_pkg "@serialport/$parser" "$out/node_modules/@serialport/$parser"
|
copy_pnpm_pkg "@serialport/$parser" "$out/node_modules/@serialport/$parser"
|
||||||
done
|
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
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue