Add Nix package, NixOS module, and tests for bambuddy

- packages.bambuddy: FastAPI backend + prebuilt React frontend assembled
  into the app-root layout upstream expects, behind a single `bambuddy`
  launcher (uvicorn --loop asyncio, ffmpeg on PATH, XDG state defaults).
  Pins upstream maziggy/bambuddy v1.2.5.7.
- nixosModules.default: services.bambuddy with host/port/dataDir,
  openFirewall, environment + environmentFile, extraPackages,
  DynamicUser + StateDirectory, systemd hardening, and
  CAP_NET_BIND_SERVICE for the virtual-printer FTPS/RTSP ports.
- checks.bambuddy-import: backend import + admin CLI smoke test.
- checks.bambuddy-service: two-node NixOS VM test covering the health
  endpoint, UI/static assets/manifest, SQLite + logs in the
  StateDirectory, journal startup check, cross-node reachability with
  openFirewall, custom host/port/env node, and loopback-only binding.
- overlay, run app, devShell, README, .gitignore.
This commit is contained in:
0m.ax 2026-11-01 19:54:55 +01:00
commit 5fb8e8d724
9 changed files with 761 additions and 0 deletions

204
pkgs/bambuddy/default.nix Normal file
View file

@ -0,0 +1,204 @@
{
lib,
stdenv,
fetchFromGitHub,
buildNpmPackage,
python3,
makeWrapper,
ffmpeg,
}:
let
pname = "bambuddy";
version = "1.2.5.7";
src = fetchFromGitHub {
owner = "maziggy";
repo = "bambuddy";
rev = "188df76f8a7342542c0876dac68990ea2a8c8313"; # v1.2.5.7
hash = "sha256-KmlAdFoSIMa6Wahd4XPBvaBtpVR/5LPNMyaimuDZVb0=";
};
# Runtime Python dependencies, mirroring upstream requirements.txt.
# All of these ship in nixpkgs. Optional heavy extras (opencv4,
# fast-simplification) are included for feature parity with the upstream
# Docker image, which installs requirements.txt in full; the affected
# features (plate detection, STL thumbnails) degrade gracefully if absent.
pythonDeps =
ps: with ps; [
# Web framework
fastapi
uvicorn
uvloop
httptools
websockets
python-dotenv
# Database
sqlalchemy
aiosqlite
asyncpg
greenlet
# Pydantic / settings
pydantic
pydantic-settings
# Bambu Lab printer communication (MQTT / FTP)
paho-mqtt
aioftp
pyftpdlib
cryptography
pyopenssl
asyncssh
# 3MF processing
defusedxml
# Excel export
openpyxl
# Utilities
python-multipart
aiofiles
# QR codes / PDF generation / PDF thumbnails
qrcode
reportlab
pypdfium2
# STL thumbnails
trimesh
matplotlib
fast-simplification
networkx
lxml
# System monitoring
psutil
# Authentication
pyjwt
passlib
bcrypt
ldap3
pyotp
# HTTP clients
idna
httpx
certifi
curl-cffi
urllib3
aiohttp
# Plate detection
opencv4
numpy
pillow
];
pythonEnv = python3.withPackages pythonDeps;
# Prebuilt React frontend. `vite build` emits to `../static` (the app root,
# next to `backend/`), matching the upstream Dockerfile.
frontend = buildNpmPackage {
pname = "${pname}-web";
inherit version src;
sourceRoot = "${src.name}/frontend";
npmDepsHash = "sha256-S+WU4jgs2DPWHHfspeo5k8vPECK+5SidGa8kzI4K4lw=";
# The source tree arrives from the store read-only, but `vite build`
# empties and writes ../static (the repo ships a committed static/ dir).
preBuild = ''
chmod -R u+w "$NIX_BUILD_TOP/${src.name}"
rm -rf ../static
'';
installPhase = ''
runHook preInstall
mkdir -p $out/share
cp -r ../static $out/share/static
runHook postInstall
'';
meta = {
description = "Web frontend for Bambuddy";
license = lib.licenses.agpl3Only;
platforms = lib.platforms.unix;
};
};
in
stdenv.mkDerivation {
inherit pname version src;
dontBuild = true;
nativeBuildInputs = [ makeWrapper ];
buildInputs = [ pythonEnv ];
passthru = {
inherit pythonEnv frontend;
};
installPhase = ''
runHook preInstall
# App root: `backend/` (Python sources) + `static/` (prebuilt frontend).
# backend.app.core.config resolves both relative to this root, exactly
# like the upstream Docker image (/app/backend + /app/static).
mkdir -p $out/share/bambuddy
cp -r ${src}/backend $out/share/bambuddy/backend
cp -r ${frontend}/share/static $out/share/bambuddy/static
chmod -R u+w $out/share/bambuddy
# The upstream test suite is not shipped in the runtime package.
rm -rf $out/share/bambuddy/backend/tests
find $out/share/bambuddy -type d -name __pycache__ -prune -exec rm -rf {} +
install -Dm644 ${src}/LICENSE -t $out/share/licenses/bambuddy
mkdir -p $out/bin
install -Dm755 ${./wrapper.sh} $out/bin/bambuddy
# Substitute at build time so @out@ resolves to THIS package's path.
# (A separate replaceVars derivation would substitute its own outPath
# instead — which silently broke the PYTHONPATH in the launcher.)
substituteInPlace $out/bin/bambuddy \
--replace-warn '@out@' "$out" \
--replace-warn '@pythonEnv@' "${pythonEnv}"
# ffmpeg is used for timelapse conversion and camera stream re-encoding.
wrapProgram $out/bin/bambuddy \
--prefix PATH : ${lib.makeBinPath [ ffmpeg ]}
runHook postInstall
'';
# Fix up the interpreter reference in the substituted wrapper.
preFixup = ''
patchShebangs $out/bin/bambuddy
'';
meta = {
description = "Self-hosted command center for Bambu Lab 3D printers";
longDescription = ''
Bambuddy is a self-hosted command center for Bambu Lab 3D printers —
from a single A1 to an entire print farm. Monitor printers in real
time, archive every print automatically, slice and queue jobs locally,
and manage the whole workflow without Bambu's cloud.
This package bundles the FastAPI backend and the prebuilt React
frontend behind a single `bambuddy` launcher. Configuration is done via
environment variables (DATA_DIR, LOG_DIR, HOST, PORT, ...) — see the
NixOS module (`services.bambuddy`) or upstream `.env.example` for the
full list.
'';
homepage = "https://bambuddy.cool/";
changelog = "https://github.com/maziggy/bambuddy/releases/tag/v${version}";
license = lib.licenses.agpl3Only;
sourceProvenance = with lib.sourceTypes; [ fromSource ];
platforms = lib.platforms.unix;
mainProgram = "bambuddy";
};
}

28
pkgs/bambuddy/wrapper.sh Normal file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Bambuddy launcher.
#
# Starts the FastAPI backend via uvicorn, mirroring the upstream Docker CMD
# (`uvicorn backend.app.main:app --loop asyncio`). PYTHONPATH points at the
# app root (@out@/share/bambuddy) which contains the `backend` package and
# the prebuilt `static/` frontend — the layout backend.app.core.config expects.
set -euo pipefail
# State locations. The NixOS module always sets these explicitly; the
# defaults keep a bare `bambuddy` invocation out of the read-only store.
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bambuddy}"
export LOG_DIR="${LOG_DIR:-$DATA_DIR/logs}"
export HOME="${HOME:-$DATA_DIR}"
export MPLCONFIGDIR="${MPLCONFIGDIR:-$DATA_DIR/.cache/matplotlib}"
mkdir -p "$DATA_DIR" "$LOG_DIR" "$MPLCONFIGDIR"
export PYTHONPATH="@out@/share/bambuddy${PYTHONPATH:+:$PYTHONPATH}"
# `--loop asyncio` matches the upstream Dockerfile: uvloop has a TLS bug that
# can truncate VP FTP uploads. Override with UVICORN_LOOP=uvloop if desired.
exec "@pythonEnv@/bin/uvicorn" backend.app.main:app \
--host "${HOST:-0.0.0.0}" \
--port "${PORT:-8000}" \
--loop "${UVICORN_LOOP:-asyncio}" \
--timeout-graceful-shutdown "${UVICORN_TIMEOUT_GRACEFUL_SHUTDOWN:-5}" \
"$@"